Suggestions

This commit is contained in:
2025-07-22 13:41:13 -06:00
parent 8bb0f84fd1
commit b18c05d52a
5 changed files with 478 additions and 473 deletions

View File

@@ -106,7 +106,7 @@ function lista()
} }
// === Consulta de productos frecuentes === // === Consulta de productos frecuentes ===
$sql = "SELECT $sql = "SELECT
pf.id_producto_frecuente, pf.id_producto_frecuente,
pf.sinonimo, pf.sinonimo,
pf.fraccion, pf.fraccion,
@@ -120,16 +120,16 @@ function lista()
pf.estado_mercancia, pf.estado_mercancia,
pf.preferencia, pf.preferencia,
pf.frecuencia_uso pf.frecuencia_uso
FROM dbo.productos_frecuentes pf FROM dbo.productos_frecuentes pf
LEFT JOIN dbo.unidades_medida_apendice7 u LEFT JOIN dbo.unidades_medida_apendice7 u
ON pf.umc_id = u.id ON pf.umc_id = u.id
LEFT JOIN dbo.paises por LEFT JOIN dbo.paises por
ON pf.pais_origen_destino = por.nombre ON pf.pais_origen_destino = por.nombre
LEFT JOIN dbo.paises pc LEFT JOIN dbo.paises pc
ON pf.pais_comprador_vendedor = pc.nombre ON pf.pais_comprador_vendedor = pc.nombre
WHERE pf.id_importador = ? WHERE pf.id_importador = ?
AND pf.status = 1 AND pf.status = 1
ORDER BY pf.frecuencia_uso DESC ORDER BY pf.frecuencia_uso DESC
"; ";
$stmt = sqlsrv_query($conn, $sql, [$_SESSION['usuario_id']]); $stmt = sqlsrv_query($conn, $sql, [$_SESSION['usuario_id']]);
@@ -142,9 +142,9 @@ function lista()
$productos = []; $productos = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$clave = $row['proveedor_guardado']; $clave = $row['proveedor_guardado'];
$row['nombre_proveedor'] = $proveedoresApi[$clave] ?? 'N/D'; $row['nombre_proveedor'] = $proveedoresApi[$clave] ?? 'N/D';
$productos[] = $row; $productos[] = $row;
} }
// === Cargar la vista con productos y nombres de proveedor === // === Cargar la vista con productos y nombres de proveedor ===
@@ -369,18 +369,20 @@ function guardar()
} }
// Convertir valores vacíos a NULL para campos opcionales // Convertir valores vacíos a NULL para campos opcionales
if ($numero_parte === '') $numero_parte = null; if ($numero_parte === '') $numero_parte = null;
if ($descripcion === '') $descripcion = null; if ($descripcion === '') $descripcion = null;
if ($uso_mercancia === '') $uso_mercancia = null; if ($uso_mercancia === '') $uso_mercancia = null;
if ($estado_mercancia === '') $estado_mercancia = null; if ($estado_mercancia === '') $estado_mercancia = null;
if ($vinculacion === '') $vinculacion = null; if ($vinculacion === '') $vinculacion = null;
if ($observaciones === '') $observaciones = null; if ($observaciones === '') $observaciones = null;
if ($preferencia === '') $preferencia = null; if ($preferencia === '') $preferencia = null;
if ($criterio_preferencia === '') $criterio_preferencia = null; if ($criterio_preferencia === '') $criterio_preferencia = null;
if ($uso_producto === '') $uso_producto = null; if ($uso_producto === '') $uso_producto = null;
if ($descripcion_producto === '') $descripcion_producto = null; if ($descripcion_producto === '') $descripcion_producto = null;
if ($tipo_mercancia === '') $tipo_mercancia = null; if ($tipo_mercancia === '') $tipo_mercancia = null;
if ($proveedor === '') $proveedor = null; if ($proveedor === '') $proveedor = null;
if ($certificado_origen === '') $certificado_origen = null;
if ($documento_en_original === '') $documento_en_original = null;
// INSERT // INSERT
$sql = "INSERT INTO dbo.productos_frecuentes $sql = "INSERT INTO dbo.productos_frecuentes

View File

@@ -80,13 +80,11 @@
.form-group-animated:nth-child(10) { animation-delay: 1.0s; } .form-group-animated:nth-child(10) { animation-delay: 1.0s; }
@keyframes slideUp { to { opacity: 1; transform: translateY(0); } } @keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
/* Estilos para las sugerencias de autocompletado */ /* Estilos para las sugerencias de autocompletado */
.autocomplete-suggestions { position: absolute; width: calc(100% - 2px); /* Ajustar al ancho del input */ background: white; border: 1px solid #ced4da; border-top: none; #global-autocomplete-suggestions { box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; border: 1px solid #dee2e6 !important; width: calc(100% - 30px) !important; /* Ajusta según el padding de tu contenedor */ }
border-radius: 0 0 4px 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 1000; max-height: 200px; overflow-y: auto; display: none; /* Inicialmente oculto */ } #global-autocomplete-suggestions .autocomplete-item { transition: background-color 0.15s ease; }
.autocomplete-item { padding: 8px 12px; cursor: pointer; transition: background-color 0.2s; } #global-autocomplete-suggestions .autocomplete-item:hover,
.autocomplete-item:hover, .autocomplete-item.active { background-color: #f8f9fa; } #global-autocomplete-suggestions .autocomplete-item.active { background-color: #f8f9fa !important; }
.autocomplete-item strong { display: block; margin-bottom: 2px; } #global-autocomplete-suggestions .autocomplete-item:last-child { border-bottom: none; }
.autocomplete-item .text-muted { font-size: 0.85em; color: #6c757d; }
.cursor-pointer { cursor: pointer; }
/* Estilos para validación */ /* Estilos para validación */
.choices.required .choices__inner { border: 1px solid #ced4da; } .choices.required .choices__inner { border: 1px solid #ced4da; }
.choices.is-invalid .choices__inner { border: 1px solid #dc3545; background-color: #fff5f5; } .choices.is-invalid .choices__inner { border: 1px solid #dc3545; background-color: #fff5f5; }
@@ -666,142 +664,243 @@
$(document).ready(function() { $(document).ready(function() {
let timeoutId; let timeoutId;
let currentRequest = null; let currentRequest = null;
let currentInput = null; // Para rastrear qué input está activo
// Crear un único contenedor de sugerencias al final del body
if ($('#global-autocomplete-suggestions').length === 0) {
$('#tabla-partidas').after(`
<div id="global-autocomplete-suggestions"
class="bg-white border shadow-lg w-100"
style="display: none;
max-height: 200px;
overflow-y: auto;
border-radius: 4px;
margin-top: 5px;
margin-bottom: 10px;">
</div>
`);
}
const $globalSuggestions = $('#global-autocomplete-suggestions');
// Función para configurar autocompletado en un input // Función para configurar autocompletado en un input
function setupAutocomplete(input) { function setupAutocomplete(input) {
const $input = $(input); const $input = $(input);
const $row = $input.closest('tr');
// Crear contenedor de sugerencias si no existe
if ($input.siblings('.autocomplete-suggestions').length === 0) {
$input.after('<div class="autocomplete-suggestions position-absolute w-100 bg-white border border-top-0 shadow-sm" style="max-height: 200px; overflow-y: auto; z-index: 1000; display: none;"></div>');
}
const $suggestions = $input.siblings('.autocomplete-suggestions');
const $row = $input.closest('tr');
$input.off('input').on('input', function() { $input.off('input').on('input', function() {
const query = $(this).val().trim(); const query = $(this).val().trim();
currentInput = $input; // Guardar referencia del input activo
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (currentRequest) currentRequest.abort(); if (currentRequest) currentRequest.abort();
if (query.length < 2) { if (query.length < 2) {
$suggestions.hide().empty(); $globalSuggestions.hide().empty();
return; return;
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
currentRequest = $.ajax({ currentRequest = $.ajax({
url: '/IMPORTADORES/solicitud_importacion/buscar_productos', url: '/IMPORTADORES/solicitud_importacion/buscar_productos',
method: 'GET', method: 'GET',
data: { q: query }, data: { q: query },
dataType: 'json', dataType: 'json',
success: function(productos) { success: function(productos) {
$suggestions.empty(); $globalSuggestions.empty();
if (productos && productos.length > 0) { if (productos && productos.length > 0) {
productos.forEach(function(producto) { productos.forEach(function(producto) {
const $item = $(` const $item = $(`
<div class="autocomplete-item px-3 py-2 cursor-pointer border-bottom"> <div class="autocomplete-item px-3 py-2 cursor-pointer border-bottom hover-bg-light"
<strong>${producto.sinonimo}</strong> style="cursor: pointer;">
<div class="row mb-3 small text-muted"> <strong style="color: #333;">${producto.sinonimo}</strong>
<span>${producto.fraccion || 'Sin fracción'}</span> <div class="small text-muted mt-1">
<span>${producto.descripcion?.substring(0, 50) || ''}</span> <div>Fracción: ${producto.fraccion || 'Sin fracción'}</div>
<div>Desc: ${producto.descripcion?.substring(0, 50) || ''}${producto.descripcion?.length > 50 ? '...' : ''}</div>
</div> </div>
</div> </div>
`).data('producto', producto); `).data('producto', producto);
$item.on('click', function() { selectProduct($(this).data('producto'), $row); }); // Efectos hover
$suggestions.append($item); $item.on('mouseenter', function() {
$globalSuggestions.find('.autocomplete-item').removeClass('active');
$(this).addClass('active').css('background-color', '#f8f9fa');
}).on('mouseleave', function() {
$(this).removeClass('active').css('background-color', '');
});
$item.on('click', function() {
selectProduct($(this).data('producto'), currentInput.closest('tr'));
});
$globalSuggestions.append($item);
}); });
$suggestions.show();
// Posicionar y mostrar
$globalSuggestions.show();
}
else {
$globalSuggestions.hide();
} }
else { $suggestions.hide(); }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
if (status !== 'abort') { console.error("Error en búsqueda:", error); } if (status !== 'abort') {
$suggestions.hide(); console.error("Error en búsqueda:", error);
}
$globalSuggestions.hide();
}, },
complete: function() { currentRequest = null; } complete: function() {
currentRequest = null;
}
}); });
}, 300); }, 300);
}); });
// Manejar eventos de foco
$input.on('focus', function() {
currentInput = $input;
});
// Manejar teclado // Manejar teclado
$input.on('keydown', function(e) { $input.on('keydown', function(e) {
const $items = $suggestions.find('.autocomplete-item'); if (!$globalSuggestions.is(':visible')) return;
const $items = $globalSuggestions.find('.autocomplete-item');
const $active = $items.filter('.active'); const $active = $items.filter('.active');
switch(e.key) { switch(e.key) {
case 'ArrowDown': case 'ArrowDown':
e.preventDefault(); e.preventDefault();
$items.removeClass('active').css('background-color', '');
const $next = $active.length ? $active.next() : $items.first(); const $next = $active.length ? $active.next() : $items.first();
$items.removeClass('active'); $next.addClass('active').css('background-color', '#f8f9fa');
$next.addClass('active');
// Scroll automático
const containerHeight = $globalSuggestions.height();
const itemTop = $next.position().top;
const itemHeight = $next.outerHeight();
if (itemTop + itemHeight > containerHeight) {
$globalSuggestions.scrollTop($globalSuggestions.scrollTop() + itemHeight);
}
break; break;
case 'ArrowUp': case 'ArrowUp':
e.preventDefault(); e.preventDefault();
$items.removeClass('active').css('background-color', '');
const $prev = $active.length ? $active.prev() : $items.last(); const $prev = $active.length ? $active.prev() : $items.last();
$items.removeClass('active'); $prev.addClass('active').css('background-color', '#f8f9fa');
$prev.addClass('active');
// Scroll automático
if ($prev.position().top < 0) {
$globalSuggestions.scrollTop($globalSuggestions.scrollTop() - $prev.outerHeight());
}
break; break;
case 'Enter': case 'Enter':
if ($active.length) { if ($active.length) {
e.preventDefault(); e.preventDefault();
selectProduct($active.data('producto'), $row); selectProduct($active.data('producto'), $input.closest('tr'));
} }
break; break;
case 'Tab': case 'Tab':
if ($suggestions.is(':visible')) { if ($globalSuggestions.is(':visible')) {
e.preventDefault(); e.preventDefault();
const $first = $items.first(); const $first = $items.first();
if ($first.length) selectProduct($first.data('producto'), $row); if ($first.length) {
selectProduct($first.data('producto'), $input.closest('tr'));
}
} }
break; break;
case 'Escape': case 'Escape':
$suggestions.hide(); $globalSuggestions.hide();
break; break;
} }
}); });
// Ocultar al hacer clic fuera
$(document).on('click', function(e) { if (!$input.is(e.target) && !$suggestions.is(e.target) && !$suggestions.has(e.target).length) { $suggestions.hide(); } });
} }
// Ocultar sugerencias al hacer clic fuera o al hacer scroll
$(document).on('click', function(e) {
if (!currentInput ||
(!currentInput.is(e.target) &&
!$globalSuggestions.is(e.target) &&
!$globalSuggestions.has(e.target).length)) {
$globalSuggestions.hide();
}
});
// Ocultar al hacer scroll (opcional)
$(window).on('scroll resize', function() {
if ($globalSuggestions.is(':visible') && currentInput) {
// Reposicionar en lugar de ocultar
positionGlobalSuggestions(currentInput);
}
});
// Función para seleccionar un producto // Función para seleccionar un producto
function selectProduct(producto, $row) { function selectProduct(producto, $row) {
// Campos básicos // Campos básicos
$row.find('input[name*="[descripcion]"]').val(producto.sinonimo); $row.find('input[name*="[descripcion]"]').val(producto.sinonimo);
// Tasa preferencial // Tasa preferencial
if (producto.preferencia) { if (producto.preferencia) {
const $select = $row.find('select[name*="[tasa_preferencial]"]'); const $select = $row.find('select[name*="[tasa_preferencial]"]');
if ($select[0] && $select[0]._choices) { $select[0]._choices.setChoiceByValue(producto.preferencia); } if ($select[0] && $select[0]._choices) {
else { $select.val(producto.preferencia).trigger('change'); } $select[0]._choices.setChoiceByValue(producto.preferencia);
} else {
$select.val(producto.preferencia).trigger('change');
}
} }
// Unidad de medida // Unidad de medida
if (producto.umc_id) { if (producto.umc_id) {
const $select = $row.find('select[name*="[unidad_comercial_id]"]'); const $select = $row.find('select[name*="[unidad_comercial_id]"]');
setTimeout(() => { setTimeout(() => {
if ($select[0] && $select[0]._choices) { $select[0]._choices.setChoiceByValue(producto.umc_id.toString()); } if ($select[0] && $select[0]._choices) {
else { $select.val(producto.umc_id).trigger('change'); } $select[0]._choices.setChoiceByValue(producto.umc_id.toString());
} else {
$select.val(producto.umc_id).trigger('change');
}
}, 50); }, 50);
} }
// Número de parte // Número de parte
if (producto.numero_parte) { $row.find('input[name*="[oma_factura]"]').val(producto.numero_parte); } if (producto.numero_parte) {
$row.find('input[name*="[oma_factura]"]').val(producto.numero_parte);
}
// Incrementar frecuencia // Incrementar frecuencia
$.ajax({ $.ajax({
url: '/IMPORTADORES/solicitud_importacion/incrementar_frecuencia', url: '/IMPORTADORES/solicitud_importacion/incrementar_frecuencia',
method: 'POST', method: 'POST',
data: { producto_id: producto.id } data: { producto_id: producto.id }
}); });
// Ocultar sugerencias // Ocultar sugerencias
$row.find('.autocomplete-suggestions').hide(); $globalSuggestions.hide();
// Enfocar el siguiente campo si existe
const $nextInput = $row.next('tr').find('.descripcion-input');
if ($nextInput.length) {
setTimeout(() => $nextInput.focus(), 100);
}
} }
// Inicializar autocompletado en inputs existentes // Inicializar autocompletado en inputs existentes
$('.descripcion-input').each(function() { setupAutocomplete(this); }); $('.descripcion-input').each(function() {
setupAutocomplete(this);
});
// Configurar autocompletado para nuevas filas // Configurar autocompletado para nuevas filas
$('#add-partida').on('click', function() { setTimeout(function() { $('.descripcion-input').last().each(function() { setupAutocomplete(this); }); }, 100); }); $('#add-partida').on('click', function() {
setTimeout(function() {
$('.descripcion-input').last().each(function() {
setupAutocomplete(this);
});
}, 100);
});
}); });
</script> </script>

View File

@@ -81,13 +81,11 @@
.form-group-animated:nth-child(10) { animation-delay: 1.0s; } .form-group-animated:nth-child(10) { animation-delay: 1.0s; }
@keyframes slideUp { to { opacity: 1; transform: translateY(0); } } @keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
/* Estilos para las sugerencias de autocompletado */ /* Estilos para las sugerencias de autocompletado */
.autocomplete-suggestions { position: absolute; width: calc(100% - 2px); /* Ajustar al ancho del input */ background: white; border: 1px solid #ced4da; border-top: none; #global-autocomplete-suggestions { box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; border: 1px solid #dee2e6 !important; width: calc(100% - 30px) !important; /* Ajusta según el padding de tu contenedor */ }
border-radius: 0 0 4px 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 1000; max-height: 200px; overflow-y: auto; display: none; /* Inicialmente oculto */ } #global-autocomplete-suggestions .autocomplete-item { transition: background-color 0.15s ease; }
.autocomplete-item { padding: 8px 12px; cursor: pointer; transition: background-color 0.2s; } #global-autocomplete-suggestions .autocomplete-item:hover,
.autocomplete-item:hover, .autocomplete-item.active { background-color: #f8f9fa; } #global-autocomplete-suggestions .autocomplete-item.active { background-color: #f8f9fa !important; }
.autocomplete-item strong { display: block; margin-bottom: 2px; } #global-autocomplete-suggestions .autocomplete-item:last-child { border-bottom: none; }
.autocomplete-item .text-muted { font-size: 0.85em; color: #6c757d; }
.cursor-pointer { cursor: pointer; }
/* Estilos para validación */ /* Estilos para validación */
.choices.required .choices__inner { border: 1px solid #ced4da; } .choices.required .choices__inner { border: 1px solid #ced4da; }
.choices.is-invalid .choices__inner { border: 1px solid #dc3545; background-color: #fff5f5; } .choices.is-invalid .choices__inner { border: 1px solid #dc3545; background-color: #fff5f5; }
@@ -361,6 +359,26 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
<script> <script>
// Variables globales para el autocompletado
let timeoutId;
let currentRequest = null;
let currentInput = null;
// ✅ 1. INICIALIZACIÓN PRINCIPAL
document.addEventListener('DOMContentLoaded', function() {
const proveedorActual = '<?= htmlspecialchars($proveedor_clave_actual ?? '') ?>';
console.log('📦 Proveedor actual:', proveedorActual);
// Inicializar Choices.js en todos los selects
cargarProveedores(proveedorActual).then(() => {
document.querySelectorAll('.searchable').forEach(safeInitializeChoices);
});
// Inicializar el sistema de autocompletado
initAutocompleteSystem();
});
// ✅ 2. FUNCIÓN PARA INICIALIZAR CHOICES.JS
function safeInitializeChoices(element) { function safeInitializeChoices(element) {
if (!element._choices) { if (!element._choices) {
const isRequired = element.hasAttribute('required'); const isRequired = element.hasAttribute('required');
@@ -370,45 +388,47 @@
} }
element._choices = new Choices(element, { element._choices = new Choices(element, {
searchEnabled: true, searchEnabled: true,
itemSelectText: '', itemSelectText: '',
shouldSort: false, shouldSort: false,
silent: true silent: true
}); });
if (isRequired) { if (isRequired) {
const container = element.closest('.choices'); const container = element.closest('.choices');
if (container) { container.classList.add('required'); } if (container) container.classList.add('required');
element.addEventListener('change', validateChoice.bind(null, element)); element.addEventListener('change', validateChoice.bind(null, element));
} }
} }
} }
// ✅ 1. INICIALIZACIÓN PRINCIPAL // ✅ 3. FUNCIÓN PARA VALIDAR CAMPOS CHOICES
document.addEventListener('DOMContentLoaded', function() { function validateChoice(selectElement) {
const proveedorActual = '<?= htmlspecialchars($proveedor_clave_actual ?? '') ?>'; const choicesInstance = selectElement._choices;
console.log('📦 Proveedor actual:', proveedorActual); if (choicesInstance) {
const container = selectElement.closest('.choices');
const hasValue = choicesInstance.getValue(true).length > 0;
const isRequired = selectElement.hasAttribute('data-required');
cargarProveedores(proveedorActual).then(() => { if (isRequired) {
document.querySelectorAll('.searchable').forEach(el => { if (hasValue) {
// Destruir instancia previa si existe container.classList.remove('is-invalid');
if (el.choicesInstance) el.choicesInstance.destroy(); container.classList.add('is-valid');
} else {
container.classList.remove('is-valid');
container.classList.add('is-invalid');
}
}
}
}
const instance = new Choices(el, { // ✅ 4. FUNCIÓN PARA CARGAR PROVEEDORES
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
// Guardar instancia
el.choicesInstance = instance;
});
});
});
// ✅ 2. FUNCIÓN PARA CARGAR PROVEEDORES (CON PROMESA)
function cargarProveedores(proveedorActual) { function cargarProveedores(proveedorActual) {
const proveedorEl = document.getElementById('proveedor_id'); const proveedorEl = document.getElementById('proveedor_id');
// Asegurarse que el nombre sea consistente
proveedorEl.name = 'proveedor_id';
return fetch('/IMPORTADORES/solicitud_importacion/ajax_proveedores') return fetch('/IMPORTADORES/solicitud_importacion/ajax_proveedores')
.then(res => { .then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -418,21 +438,19 @@
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>'; proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
json.results.forEach(item => { json.results.forEach(item => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = item.id; opt.value = item.id;
opt.textContent = item.text; opt.textContent = item.text;
if (String(item.id) === proveedorActual) { if (String(item.id) === proveedorActual) {
opt.selected = true; opt.selected = true;
} }
proveedorEl.appendChild(opt); proveedorEl.appendChild(opt);
}); });
// Forzar manualmente el value // Forzar manualmente el value
proveedorEl.value = proveedorActual; if (proveedorActual) {
console.log('✅ Proveedores cargados. Proveedor actual:', proveedorActual); proveedorEl.value = proveedorActual;
console.log('🧪 Select value actual (después de asignar):', proveedorEl.value); }
}) })
.catch(err => { .catch(err => {
console.error('❌ Error cargando proveedores:', err); console.error('❌ Error cargando proveedores:', err);
@@ -440,25 +458,22 @@
}); });
} }
// ✅ 3. AGREGAR PARTIDAS - VERSIÓN CORREGIDA // ✅ 5. AGREGAR PARTIDAS
document.getElementById('add-partida').addEventListener('click', () => { document.getElementById('add-partida').addEventListener('click', () => {
const tbody = document.querySelector('#tabla-partidas tbody'); const tbody = document.querySelector('#tabla-partidas tbody');
const rows = tbody.querySelectorAll('tr'); const idx = tbody.querySelectorAll('tr').length;
const idx = rows.length; // Esto asegura índices únicos y secuenciales const row = document.createElement('tr');
const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<td> <td>
<div class="position-relative"> <div class="position-relative">
<input name="partidas[${idx}][descripcion]" class="form-control w-auto descripcion-input" required autocomplete="off"> <input name="partidas[${idx}][descripcion]" class="form-control w-auto descripcion-input" required autocomplete="off">
<div class="autocomplete-suggestions position-absolute w-100 bg-white border border-top-0 shadow-sm"
style="max-height: 200px; overflow-y: auto; z-index: 1000; display: none;"></div>
</div> </div>
</td> </td>
<td><input name="partidas[${idx}][cantidad_comercial]" type="number" step="0.0001" class="form-control w-auto" min="0" required></td> <td><input name="partidas[${idx}][cantidad_comercial]" type="number" step="0.0001" class="form-control w-auto" min="0" required></td>
<td><input name="partidas[${idx}][cantidad_tarifa]" type="number" step="0.0001" class="form-control w-auto" min="0"></td> <td><input name="partidas[${idx}][cantidad_tarifa]" type="number" step="0.0001" class="form-control w-auto" min="0"></td>
<td> <td>
<select name="partidas[${idx}][unidad_comercial_id]" class="form-select searchable"> <select name="partidas[${idx}][unidad_comercial_id]" class="form-select searchable" data-required="true">
<option value="">-- Unidad --</option> <option value="">-- Unidad --</option>
<?php foreach($unidades_medida as $um): ?> <?php foreach($unidades_medida as $um): ?>
<option value="<?= htmlspecialchars($um['id']) ?>"><?= htmlspecialchars($um['descripcion']) ?></option> <option value="<?= htmlspecialchars($um['id']) ?>"><?= htmlspecialchars($um['descripcion']) ?></option>
@@ -468,7 +483,7 @@
<td><input name="partidas[${idx}][valor_factura]" type="number" step="0.01" class="form-control valor-partida w-auto" min="0"></td> <td><input name="partidas[${idx}][valor_factura]" type="number" step="0.01" class="form-control valor-partida w-auto" min="0"></td>
<td><input name="partidas[${idx}][peso_bruto]" type="number" step="0.0001" class="form-control w-auto" min="0"></td> <td><input name="partidas[${idx}][peso_bruto]" type="number" step="0.0001" class="form-control w-auto" min="0"></td>
<td> <td>
<select name="partidas[${idx}][tasa_preferencial]" class="form-select searchable"> <select name="partidas[${idx}][tasa_preferencial]" class="form-select searchable" data-required="true">
<option value="">-- Selecciona --</option> <option value="">-- Selecciona --</option>
<option>General</option> <option>General</option>
<option>TLC</option> <option>TLC</option>
@@ -484,39 +499,29 @@
tbody.appendChild(row); tbody.appendChild(row);
// Inicializamos Choices en los nuevos selects // Inicializar Choices en los nuevos selects
setTimeout(() => { setTimeout(() => {
row.querySelectorAll('.searchable').forEach(el => { row.querySelectorAll('.searchable').forEach(el => {
if (!el._choices) { safeInitializeChoices(el);
el._choices = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false,
silent: true
});
}
}); });
// Configurar autocompletado para el nuevo campo de descripción // Configurar autocompletado para el nuevo campo
setupAutocomplete(row.querySelector('.descripcion-input')); setupAutocomplete(row.querySelector('.descripcion-input'));
}, 50); }, 50);
}); });
// ✅ 4. REMOVER PARTIDAS // ✅ 6. REMOVER PARTIDAS
document.querySelector('#tabla-partidas tbody').addEventListener('click', e => { document.querySelector('#tabla-partidas tbody').addEventListener('click', e => {
if (e.target.matches('.remove-row')) { if (e.target.matches('.remove-row')) {
// Destruir instancia de Choices.js antes de remover la fila
const row = e.target.closest('tr'); const row = e.target.closest('tr');
row.querySelectorAll('.searchable').forEach(el => { row.querySelectorAll('.searchable').forEach(el => {
if (el.choicesInstance) { if (el._choices) el._choices.destroy();
el.choicesInstance.destroy();
}
}); });
row.remove(); row.remove();
} }
}); });
// ✅ 5. CONTROLAR OVERFLOW DE LA TABLA // ✅ 7. CONTROLAR OVERFLOW DE LA TABLA
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const tableContainer = document.querySelector('.table-responsive'); const tableContainer = document.querySelector('.table-responsive');
if (!tableContainer) return; if (!tableContainer) return;
@@ -528,335 +533,234 @@
} }
}); });
// ✅ 6. VALIDACIÓN DEL FORMULARIO // ✅ 8. VALIDACIÓN DEL FORMULARIO
$(document).ready(function() { document.getElementById('solicitudForm').addEventListener('submit', function(e) {
$('#solicitudForm').submit(function(e){ // Validar selects de Choices.js
const total = parseFloat($('#valor_factura').val()) || 0; let isValid = true;
let sum = 0;
$('.valor-partida').each(function(){ sum += parseFloat($(this).val()) || 0; });
if(Math.abs(sum - total) > 0.001){ document.querySelectorAll('.searchable[data-required]').forEach(el => {
e.preventDefault(); validateChoice(el);
Swal.fire({ icon: 'error', title: 'Error de validación', text: `La suma de partidas (${sum.toFixed(2)}) no coincide con Valor Factura (${total.toFixed(2)}).` }); const choicesInstance = el._choices;
if (choicesInstance && choicesInstance.getValue(true).length === 0) {
isValid = false;
// Desplazarse al primer error
if (isValid === false) {
el.closest('.choices').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} }
}); });
// Validar suma de partidas
const total = parseFloat(document.getElementById('valor_factura').value) || 0;
let sum = 0;
document.querySelectorAll('.valor-partida').forEach(input => {
sum += parseFloat(input.value) || 0;
});
if (Math.abs(sum - total) > 0.001) {
e.preventDefault();
Swal.fire({
icon: 'error',
title: 'Error de validación',
text: `La suma de partidas (${sum.toFixed(2)}) no coincide con Valor Factura (${total.toFixed(2)}).`
});
return;
}
if (!isValid) {
e.preventDefault();
Swal.fire({
icon: 'error',
title: 'Campos requeridos',
text: 'Por favor complete todos los campos obligatorios marcados en rojo',
confirmButtonColor: '#3085d6'
});
}
}); });
document.addEventListener('DOMContentLoaded', function() { // ✅ 9. SISTEMA DE AUTOCOMPLETADO
const transportistaSelect = document.getElementById('transportista_id'); function initAutocompleteSystem() {
const choferSelect = document.getElementById('chofer_id'); // Crear contenedor de sugerencias si no existe
if (!document.getElementById('global-autocomplete-suggestions')) {
const suggestionsDiv = document.createElement('div');
suggestionsDiv.id = 'global-autocomplete-suggestions';
suggestionsDiv.className = 'bg-white border shadow-sm w-100';
suggestionsDiv.style.cssText = 'display: none; max-height: 200px; overflow-y: auto; border-radius: 4px; margin-top: 5px; margin-bottom: 10px;';
document.querySelector('#tabla-partidas').after(suggestionsDiv);
}
// Guardar todas las opciones de choferes originales (para fallback) // Configurar autocompletado para inputs existentes
const allChoferes = Array.from(choferSelect.querySelectorAll('option')).slice(1); // Excluir la primera opción vacía document.querySelectorAll('.descripcion-input').forEach(input => {
setupAutocomplete(input);
// Obtener valores actuales (para modo edición)
const transportistaActual = transportistaSelect.value;
const choferActual = choferSelect.value;
// Crear un mapa de choferes por transportista desde las opciones cargadas
const choferesPorTransportista = {};
allChoferes.forEach(option => {
const transportistaId = option.getAttribute('data-transportista');
if (transportistaId) {
if (!choferesPorTransportista[transportistaId]) {
choferesPorTransportista[transportistaId] = [];
}
choferesPorTransportista[transportistaId].push({
id_chofer: option.value,
nombre: option.textContent.trim()
});
}
}); });
// Debug: mostrar el cache inicial
console.log('Cache de choferes por transportista:', choferesPorTransportista);
// Inicializar Choices.js si está disponible
let choferChoices = null;
if (typeof Choices !== 'undefined') {
choferChoices = new Choices(choferSelect, {
searchEnabled: true,
placeholderValue: '-- Selecciona Transportista primero --',
noResultsText: 'No se encontraron resultados',
itemSelectText: '',
searchPlaceholderValue: 'Buscar chofer...'
});
}
// Función para filtrar choferes por transportista
function filtrarChoferes(transportistaId) {
// Limpiar opciones excepto la primera
if (choferChoices) { choferChoices.clearStore(); }
else { choferSelect.innerHTML = '<option value="">-- Selecciona Chofer --</option>'; }
if (transportistaId) {
// Intentar usar datos locales primero (más rápido)
if (choferesPorTransportista[transportistaId] && choferesPorTransportista[transportistaId].length > 0) {
console.log('Usando datos locales para transportista:', transportistaId);
cargarChoferes(choferesPorTransportista[transportistaId]);
} else {
// Si no hay datos locales, hacer petición AJAX
console.log('Haciendo petición AJAX para transportista:', transportistaId);
mostrarCargando();
fetch(`/IMPORTADORES/solicitud_importacion/obtenerChoferesPorTransportista?transportista_id=${transportistaId}`)
.then(response => {
console.log('Response status:', response.status);
if (!response.ok) { return response.json().then(data => { throw new Error(data.error || `Error ${response.status}`); }); }
return response.json();
})
.then(choferes => {
console.log('Choferes recibidos:', choferes);
// Guardar en cache local para futuras consultas
choferesPorTransportista[transportistaId] = choferes;
cargarChoferes(choferes);
})
.catch(error => {
console.error('Error al cargar choferes:', error);
mostrarError();
});
}
}
else {
// Si no hay transportista seleccionado, mostrar mensaje
mostrarSeleccionarTransportista();
}
}
// Función para mostrar estado de carga
function mostrarCargando() {
if (choferChoices) {
choferChoices.clearStore();
choferChoices.setChoices([{
value: '',
label: '-- Cargando choferes... --',
disabled: true
}], 'value', 'label', true);
}
else { choferSelect.innerHTML = '<option value="">-- Cargando choferes... --</option>'; }
}
// Función para mostrar mensaje de seleccionar transportista
function mostrarSeleccionarTransportista() {
if (choferChoices) {
choferChoices.clearStore();
choferChoices.setChoices([{
value: '',
label: '-- Selecciona Transportista primero --',
disabled: true
}], 'value', 'label', true);
}
else { choferSelect.innerHTML = '<option value="">-- Selecciona Transportista primero --</option>'; }
}
// Función para mostrar error
function mostrarError() {
const errorMessage = '-- Error al cargar choferes --';
if (choferChoices) {
choferChoices.clearStore();
choferChoices.setChoices([{
value: '',
label: errorMessage,
disabled: true
}], 'value', 'label', true);
}
else { choferSelect.innerHTML = `<option value="">${errorMessage}</option>`; }
}
// Función para cargar choferes en el select
function cargarChoferes(choferes) {
const opciones = [{
value: '',
label: '-- Selecciona Chofer --',
disabled: false
}];
choferes.forEach(chofer => {
opciones.push({
value: chofer.id_chofer,
label: chofer.nombre,
disabled: false
});
});
if (choferChoices) {
choferChoices.clearStore();
choferChoices.setChoices(opciones, 'value', 'label', true);
// Restaurar selección actual si es válida
if (choferActual && choferes.some(c => c.id_chofer == choferActual)) {
choferChoices.setChoiceByValue(choferActual);
}
} else {
choferSelect.innerHTML = '';
opciones.forEach(opcion => {
const option = document.createElement('option');
option.value = opcion.value;
option.textContent = opcion.label;
if (opcion.disabled) option.disabled = true;
choferSelect.appendChild(option);
});
// Restaurar selección actual si es válida
if (choferActual && choferes.some(c => c.id_chofer == choferActual)) { choferSelect.value = choferActual; }
}
}
// Inicializar el filtro al cargar la página (para modo edición)
if (transportistaActual) { filtrarChoferes(transportistaActual); }
else { mostrarSeleccionarTransportista(); }
// Event listener para cambio de transportista
transportistaSelect.addEventListener('change', function() {
const transportistaId = this.value;
filtrarChoferes(transportistaId);
});
// Event listener para validar que se seleccione un chofer válido
choferSelect.addEventListener('change', function() {
const choferSeleccionado = this.value;
if (choferSeleccionado && !transportistaSelect.value) {
alert('Por favor, selecciona un transportista primero.');
this.value = '';
if (choferChoices) { choferChoices.setChoiceByValue(''); }
}
});
});
$(document).ready(function() {
let timeoutId;
let currentRequest = null;
// Función para configurar autocompletado
function setupAutocomplete(input) {
const $input = $(input);
// Crear contenedor de sugerencias si no existe
if ($input.siblings('.autocomplete-suggestions').length === 0) { $input.after('<div class="autocomplete-suggestions position-absolute w-100 bg-white border border-top-0 shadow-sm" style="max-height: 200px; overflow-y: auto; z-index: 1000; display: none;"></div>'); }
const $suggestions = $input.siblings('.autocomplete-suggestions');
const $row = $input.closest('tr');
$input.on('input', function() {
const query = $(this).val().trim();
clearTimeout(timeoutId);
if (currentRequest) currentRequest.abort();
if (query.length < 2) {
$suggestions.hide().empty();
return;
}
timeoutId = setTimeout(() => {
currentRequest = $.ajax({
url: '/IMPORTADORES/solicitud_importacion/buscar_productos',
method: 'GET',
data: { q: query },
dataType: 'json',
success: function(productos) {
$suggestions.empty();
if (productos && productos.length > 0) {
productos.forEach(function(producto) {
const $item = $(`
<div class="autocomplete-item px-3 py-2 cursor-pointer border-bottom">
<strong>${producto.sinonimo}</strong>
<div class="row mb-3 small text-muted">
<span>${producto.fraccion || 'Sin fracción'}</span>
<span>${producto.descripcion?.substring(0, 50) || ''}</span>
</div>
</div>
`).data('producto', producto);
$item.on('click', function() { selectProduct($(this).data('producto'), $row); });
$suggestions.append($item);
});
$suggestions.show();
}
else { $suggestions.hide(); }
},
error: function(xhr, status, error) {
if (status !== 'abort') { console.error("Error en búsqueda:", error); }
$suggestions.hide();
},
complete: function() { currentRequest = null; }
});
}, 300);
});
// Manejar teclado
$input.on('keydown', function(e) {
const $items = $suggestions.find('.autocomplete-item');
const $active = $items.filter('.active');
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
const $next = $active.length ? $active.next() : $items.first();
$items.removeClass('active');
$next.addClass('active');
break;
case 'ArrowUp':
e.preventDefault();
const $prev = $active.length ? $active.prev() : $items.last();
$items.removeClass('active');
$prev.addClass('active');
break;
case 'Enter':
if ($active.length) {
e.preventDefault();
selectProduct($active.data('producto'), $row);
}
break;
case 'Tab':
if ($suggestions.is(':visible')) {
e.preventDefault();
const $first = $items.first();
if ($first.length) selectProduct($first.data('producto'), $row);
}
break;
case 'Escape':
$suggestions.hide();
break;
}
});
// Ocultar al hacer clic fuera
$(document).on('click', function(e) { if (!$input.is(e.target) && !$suggestions.is(e.target) && !$suggestions.has(e.target).length) { $suggestions.hide(); } });
}
// Función para seleccionar un producto
function selectProduct(producto, $row) {
// Campos básicos
$row.find('input[name*="[descripcion]"]').val(producto.sinonimo);
// Tasa preferencial
if (producto.preferencia) {
const $select = $row.find('select[name*="[tasa_preferencial]"]');
if ($select[0] && $select[0]._choices) { $select[0]._choices.setChoiceByValue(producto.preferencia); }
else { $select.val(producto.preferencia).trigger('change'); }
}
// Unidad de medida
if (producto.umc_id) {
const $select = $row.find('select[name*="[unidad_comercial_id]"]');
setTimeout(() => {
if ($select[0] && $select[0]._choices) { $select[0]._choices.setChoiceByValue(producto.umc_id.toString()); }
else { $select.val(producto.umc_id).trigger('change'); }
}, 50);
}
// Número de parte
if (producto.numero_parte) { $row.find('input[name*="[oma_factura]"]').val(producto.numero_parte); }
// Incrementar frecuencia
$.ajax({
url: '/IMPORTADORES/solicitud_importacion/incrementar_frecuencia',
method: 'POST',
data: { producto_id: producto.id }
});
// Ocultar sugerencias
$row.find('.autocomplete-suggestions').hide();
}
// Inicializar autocompletado en inputs existentes
$('.descripcion-input').each(function() { setupAutocomplete(this); });
// Configurar autocompletado para nuevas filas // Configurar autocompletado para nuevas filas
$('#add-partida').on('click', function() { setTimeout(function() { $('.descripcion-input').last().each(function() { setupAutocomplete(this); }); }, 100); }); document.getElementById('add-partida').addEventListener('click', function() {
}); setTimeout(() => {
const newInput = document.querySelector('#tabla-partidas tbody tr:last-child .descripcion-input');
if (newInput) setupAutocomplete(newInput);
}, 100);
});
}
function setupAutocomplete(input) {
const $input = $(input);
const $globalSuggestions = $('#global-autocomplete-suggestions');
$input.off('input').on('input', function() {
const query = $(this).val().trim();
currentInput = $input;
clearTimeout(timeoutId);
if (currentRequest) currentRequest.abort();
if (query.length < 2) {
$globalSuggestions.hide().empty();
return;
}
timeoutId = setTimeout(() => {
currentRequest = $.ajax({
url: '/IMPORTADORES/solicitud_importacion/buscar_productos',
method: 'GET',
data: { q: query },
dataType: 'json',
success: function(productos) {
$globalSuggestions.empty();
if (productos && productos.length > 0) {
productos.forEach(function(producto) {
const $item = $(`
<div class="autocomplete-item px-3 py-2 cursor-pointer border-bottom hover-bg-light">
<strong style="color: #333;">${producto.sinonimo}</strong>
<div class="small text-muted mt-1">
<div>Fracción: ${producto.fraccion || 'Sin fracción'}</div>
<div>Desc: ${producto.descripcion?.substring(0, 50) || ''}${producto.descripcion?.length > 50 ? '...' : ''}</div>
</div>
</div>
`).data('producto', producto);
$item.on('mouseenter', function() {
$globalSuggestions.find('.autocomplete-item').removeClass('active');
$(this).addClass('active').css('background-color', '#f8f9fa');
}).on('mouseleave', function() {
$(this).removeClass('active').css('background-color', '');
});
$item.on('click', function() {
selectProduct($(this).data('producto'), $input.closest('tr'));
});
$globalSuggestions.append($item);
});
$globalSuggestions.show();
}
},
error: function(xhr, status, error) {
if (status !== 'abort') console.error("Error en búsqueda:", error);
$globalSuggestions.hide();
},
complete: function() {
currentRequest = null;
}
});
}, 300);
});
// Manejar eventos de teclado
$input.on('keydown', function(e) {
if (!$globalSuggestions.is(':visible')) return;
const $items = $globalSuggestions.find('.autocomplete-item');
const $active = $items.filter('.active');
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
$items.removeClass('active').css('background-color', '');
const $next = $active.length ? $active.next() : $items.first();
$next.addClass('active').css('background-color', '#f8f9fa');
break;
case 'ArrowUp':
e.preventDefault();
$items.removeClass('active').css('background-color', '');
const $prev = $active.length ? $active.prev() : $items.last();
$prev.addClass('active').css('background-color', '#f8f9fa');
break;
case 'Enter':
if ($active.length) {
e.preventDefault();
selectProduct($active.data('producto'), $input.closest('tr'));
}
break;
case 'Escape':
$globalSuggestions.hide();
break;
}
});
// Ocultar sugerencias al hacer clic fuera
$(document).on('click', function(e) {
if (!currentInput ||
(!currentInput.is(e.target) &&
!$globalSuggestions.is(e.target) &&
!$globalSuggestions.has(e.target).length)) {
$globalSuggestions.hide();
}
});
}
function selectProduct(producto, $row) {
// Campos básicos
$row.find('input[name*="[descripcion]"]').val(producto.sinonimo);
// Tasa preferencial
if (producto.preferencia) {
const $select = $row.find('select[name*="[tasa_preferencial]"]');
if ($select[0] && $select[0]._choices) {
$select[0]._choices.setChoiceByValue(producto.preferencia);
} else {
$select.val(producto.preferencia).trigger('change');
}
}
// Unidad de medida
if (producto.umc_id) {
const $select = $row.find('select[name*="[unidad_comercial_id]"]');
setTimeout(() => {
if ($select[0] && $select[0]._choices) {
$select[0]._choices.setChoiceByValue(producto.umc_id.toString());
} else {
$select.val(producto.umc_id).trigger('change');
}
}, 50);
}
// Número de parte
if (producto.numero_parte) {
$row.find('input[name*="[oma_factura]"]').val(producto.numero_parte);
}
// Incrementar frecuencia
$.ajax({
url: '/IMPORTADORES/solicitud_importacion/incrementar_frecuencia',
method: 'POST',
data: { producto_id: producto.id }
});
// Ocultar sugerencias
$('#global-autocomplete-suggestions').hide();
// Enfocar el siguiente campo si existe
const $nextInput = $row.next('tr').find('.descripcion-input');
if ($nextInput.length) {
setTimeout(() => $nextInput.focus(), 100);
}
}
</script> </script>
</body> </body>

View File

@@ -141,11 +141,11 @@
function confirmChangeAgency(id) { function confirmChangeAgency(id) {
Swal.fire({ Swal.fire({
title: '¿Quieres cambiar de agencia?', title: '¿Quieres usar esta agencia?',
text: 'Dejaras de usar la agencia actual y cambiarás a la nueva.', text: 'Cambiarás la agencia activa a esta.',
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'Sí, cambiar agencia', confirmButtonText: 'Sí, usar agencia',
cancelButtonText: 'Cancelar' cancelButtonText: 'Cancelar'
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {