LSC

ERP LSC

Inicia sesión para continuar

v0.1.0 ·

Subir factura

Normalizar con Claude

Sugerimos nombre canónico, familia y detectamos duplicados.

Analizando con Claude…

Buscar productos similares

Coincidencias en el maestro con score >= 45%.

Buscando…
$
${skuBadge}
`; }).join(''); tbody.querySelectorAll('.line-delete').forEach(btn => btn.addEventListener('click', e => { e.target.closest('tr').remove(); })); tbody.querySelectorAll('.btn-normalize').forEach(btn => btn.addEventListener('click', e => { const tr = e.target.closest('tr'); normalizeLine(tr); })); tbody.querySelectorAll('.btn-search-similar').forEach(btn => btn.addEventListener('click', e => { const tr = e.target.closest('tr'); searchSimilarForLine(tr); })); lucide.createIcons(); } async function normalizeLine(tr) { if (!tr) return; const desc = tr.querySelector('[data-field="description_raw"]')?.value?.trim(); const familyHint = tr.querySelector('[data-field="family_code"]')?.value || null; if (!desc) { showToast('Sin descripción', 'Completa primero la descripción de la línea', 'error'); return; } openModal('normalize'); const body = document.getElementById('normalize-body'); body.innerHTML = `
Analizando "${desc}" con Claude…
`; lucide.createIcons(); try { const res = await api.normalizeProduct(desc, familyHint); const p = res.proposal || {}; const matches = res.matches || []; body.innerHTML = `
${p.is_duplicate && p.duplicate_sku ? `
Posible duplicado detectado
Ya existe ${p.duplicate_sku} en el maestro. Considera usar ese SKU en lugar de crear uno nuevo.
` : ''}
Original (proveedor)
${desc}
${p.reasoning ? `
Razonamiento: ${p.reasoning}
` : ''} ${matches.length ? `
Productos similares en el maestro:
${matches.map(m => `
${m.sku}
${m.normalized_name || m.original_name}
${Math.round(m.score * 100)}%
`).join('')}
` : ''}
`; document.querySelector('[data-modal="normalize"]').dataset.targetIdx = tr.dataset.idx; lucide.createIcons(); } catch (err) { body.innerHTML = `
Error: ${err.message}
`; } } async function searchSimilarForLine(tr) { if (!tr) return; const desc = tr.querySelector('[data-field="description_raw"]')?.value?.trim(); const familyHint = tr.querySelector('[data-field="family_code"]')?.value || null; if (!desc) { showToast('Sin descripción', 'Completa primero la descripción', 'error'); return; } openModal('similar'); const body = document.getElementById('similar-body'); body.innerHTML = `
Buscando…
`; lucide.createIcons(); try { const matches = await api.searchProducts(desc, familyHint); if (!matches || matches.length === 0) { body.innerHTML = `
Sin coincidencias
No hay productos similares. Quizás necesites crear uno nuevo.
`; } else { body.innerHTML = `
${matches.map(m => `
${m.sku}${m.family_code}
${m.normalized_name || m.original_name}
${Math.round(m.score * 100)}%
`).join('')}
`; } document.querySelector('[data-modal="similar"]').dataset.targetIdx = tr.dataset.idx; lucide.createIcons(); } catch (err) { body.innerHTML = `
Error: ${err.message}
`; } } async function useExistingSku(sku, familyCode) { const idx = document.querySelector('[data-modal="normalize"].active')?.dataset.targetIdx ?? document.querySelector('[data-modal="similar"].active')?.dataset.targetIdx; if (idx == null) return; const tr = document.querySelector(`#lines-tbody tr[data-idx="${idx}"]`); if (!tr) return; tr.querySelector('[data-field="product_sku"]').value = sku; const fam = tr.querySelector('[data-field="family_code"]'); if (fam) fam.value = familyCode; const badgeCell = tr.querySelector('td:nth-child(7) > div'); if (badgeCell) badgeCell.innerHTML = `${sku}`; closeModal('normalize'); closeModal('similar'); showToast('SKU asignado', `${sku} aplicado`, 'success'); } async function confirmCreateProduct(idx) { const tr = document.querySelector(`#lines-tbody tr[data-idx="${idx}"]`); if (!tr) return; const normalized = document.getElementById('norm-name')?.value?.trim().toUpperCase(); const family = document.getElementById('norm-family')?.value; const original = tr.querySelector('[data-field="description_raw"]')?.value?.trim(); const unit = tr.querySelector('[data-field="unit"]')?.value || 'UN'; const unitPrice = parseFloat(tr.querySelector('[data-field="unit_price"]')?.value || 0); if (!normalized || !family) { showToast('Faltan datos', 'Nombre y familia obligatorios', 'error'); return; } try { const product = await api.createProduct({ normalized_name: normalized, original_name: original, family_code: family, unit: unit, last_unit_cost: unitPrice || null, }); tr.querySelector('[data-field="product_sku"]').value = product.sku; tr.querySelector('[data-field="family_code"]').value = product.family_code; const badgeCell = tr.querySelector('td:nth-child(7) > div'); if (badgeCell) badgeCell.innerHTML = `${product.sku} ✓ nuevo`; closeModal('normalize'); showToast('SKU creado', `${product.sku} agregado al maestro`, 'success'); } catch (err) { showToast('No se pudo crear', err.message, 'error'); } } document.getElementById('add-line-btn').addEventListener('click', () => { if (!currentInvoice) return; const items = collectLines(); items.push({ line_number: items.length+1, description_raw: '', quantity: 0, unit: 'UN', unit_price: 0, net_amount: 0, product_sku: '', family_code: '' }); renderLines(items); }); function collectLines() { const trs = document.querySelectorAll('#lines-tbody tr'); return Array.from(trs).map((tr, idx) => { const get = field => tr.querySelector(`[data-field="${field}"]`)?.value; return { line_number: idx+1, description_raw: (get('description_raw') || '').trim() || '(sin descripción)', quantity: get('quantity') || 0, unit: (get('unit') || 'UN').toUpperCase(), unit_price: get('unit_price') || 0, discount_amount: 0, net_amount: get('net_amount') || 0, product_sku: (get('product_sku') || '').trim() || null, family_code: (get('family_code') || '').trim().toUpperCase() || null, }; }); } function collectBody() { const get = id => document.getElementById('f-'+id)?.value; return { supplier_name: get('supplier_name') || null, supplier_rut: get('supplier_rut') || null, supplier_business_activity: get('supplier_business_activity') || null, supplier_address: get('supplier_address') || null, supplier_commune: get('supplier_commune') || null, supplier_city: get('supplier_city') || null, supplier_region: get('supplier_region') || null, document_type: get('document_type') || null, folio: get('folio') || null, issue_date: get('issue_date') || null, payment_terms: get('payment_terms') || 'contado', net_amount: get('net_amount') || null, tax_amount: get('tax_amount') || null, total_amount: get('total_amount') || null, items: collectLines(), }; } document.getElementById('rev-save').addEventListener('click', async () => { if (!currentInvoice) return; try { await api.updateInvoice(currentInvoice.id, collectBody()); showToast('Cambios guardados', '', 'success'); loadInvoiceReview(currentInvoice.id); } catch (err) { showToast('No se pudo guardar', err.message, 'error'); } }); document.getElementById('rev-approve').addEventListener('click', async () => { if (!currentInvoice) return; try { await api.updateInvoice(currentInvoice.id, collectBody()); await api.approveInvoice(currentInvoice.id); showToast('Factura aprobada', currentInvoice.internal_code, 'success'); navigateTo('invoices'); } catch (err) { showToast('No se pudo aprobar', err.message, 'error'); } }); document.getElementById('rev-reprocess').addEventListener('click', async () => { if (!currentInvoice) return; try { await api.reprocessInvoice(currentInvoice.id); showToast('Reprocesando con Claude', '', 'info'); navigateTo('invoices'); } catch (err) { showToast('No se pudo reprocesar', err.message, 'error'); } }); let pendingFile = null; document.getElementById('upload-btn').addEventListener('click', () => { pendingFile = null; document.getElementById('file-label').textContent = 'Haz click o arrastra una foto'; document.getElementById('upload-confirm').disabled = true; document.getElementById('upload-error').classList.add('hidden'); document.getElementById('upload-progress').classList.add('hidden'); openModal('upload'); }); document.getElementById('file-input').addEventListener('change', (e) => { const f = e.target.files[0]; if (!f) return; pendingFile = f; document.getElementById('file-label').textContent = `${f.name} (${(f.size/1024/1024).toFixed(2)} MB)`; document.getElementById('upload-confirm').disabled = false; }); document.getElementById('upload-confirm').addEventListener('click', async () => { if (!pendingFile) return; const errEl = document.getElementById('upload-error'); const progEl = document.getElementById('upload-progress'); errEl.classList.add('hidden'); progEl.classList.remove('hidden'); try { const res = await api.uploadInvoice(pendingFile); closeModal('upload'); showToast('Factura subida', `${res.internal_code} en cola`, 'success'); loadInvoices(); } catch (err) { progEl.classList.add('hidden'); errEl.textContent = err.message; errEl.classList.remove('hidden'); } }); document.querySelectorAll('a[data-view]').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); navigateTo(a.dataset.view); }); }); (async () => { try { const me = await api.me(); currentUser = me; showApp(); } catch (_) { showLogin(); } try { const h = await api.health(); document.getElementById('env-indicator').textContent = h.app + ' · ' + h.env; document.getElementById('env-pill').textContent = h.env === 'production' ? 'Producción' : 'Dev'; } catch (_) {} lucide.createIcons(); })(); 4).toFixed(2)} MB)`; document.getElementById('upload-confirm').disabled = false; }); document.getElementById('upload-confirm').addEventListener('click', async () => { if (!pendingFile) return; const errEl = document.getElementById('upload-error'); const progEl = document.getElementById('upload-progress'); errEl.classList.add('hidden'); progEl.classList.remove('hidden'); try { const res = await api.uploadInvoice(pendingFile); closeModal('upload'); showToast('Factura subida', res.internal_code + ' en cola para OCR', 'success'); loadInvoices(); } catch (err) { progEl.classList.add('hidden'); errEl.textContent = err.message; errEl.classList.remove('hidden'); } }); document.querySelectorAll('a[data-view]').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); navigateTo(a.dataset.view); }); }); igateTo(a.dataset.view); }); }); (async () => { try { const me = await api.me(); currentUser = me; showApp(); } catch (_) { showLogin(); } lucide.createIcons(); })(); } lucide.createIcons(); })(); getElementById("env-indicator").textContent = h.app + " · " + h.env; } catch (_) {} lucide.createIcons(); })();