Revisión 2.1

This commit is contained in:
2025-06-16 14:36:01 -06:00
parent c576ab5b3b
commit 4365919211
18 changed files with 601 additions and 290 deletions

View File

@@ -44,7 +44,7 @@
<option value="">-- Selecciona un transportista --</option>
<?php foreach ($transportistas as $t): ?>
<option value="<?= $t['id_transportista'] ?>">
<?= htmlspecialchars($t['nombre']) ?>
<?= htmlspecialchars($t['clave_identificador'] . ' - ' . $t['nombre'] . ' - ' . $t['ciudad_nombre'] . ' - ' . $t['domicilio']) ?>
</option>
<?php endforeach; ?>
</select>
@@ -65,6 +65,11 @@
<input name="numero_licencia" id="numero_licencia" type="text" maxlength="11" class="form-control" required>
</div>
<div class="mb-3">
<label for="numero_gafete" class="form-label">Número de Gafete</label>
<input name="numero_gafete" id="numero_gafete" type="text" maxlength="24" class="form-control" required>
</div>
<div class="mb-3">
<label for="telefono" class="form-label">Teléfono</label>
<input name="telefono" id="telefono" type="tel" maxlength="11" class="form-control">
@@ -101,6 +106,7 @@
const nombre = document.getElementById('nombre').value.trim();
const apellido = document.getElementById('apellido').value.trim();
const numero_licencia = document.getElementById('numero_licencia').value.trim();
const numero_gafete = document.getElementById('numero_gafete').value.trim();
const telefono = document.getElementById('telefono').value.trim();
const email = document.getElementById('email').value.trim();
const fecha_ingreso = document.getElementById('fecha_ingreso').value;
@@ -132,6 +138,11 @@
document.getElementById('numero_licencia').focus();
return;
}
if (!numero_gafete) {
Swal.fire({ icon: 'error', title: 'Número de Gafete requerido', text: 'Por favor ingresa el número de gafete.', confirmButtonColor: '#dc3545' });
document.getElementById('numero_gafete').focus();
return;
}
if (!soloNumerosRegex.test(numero_licencia)) {
Swal.fire({ icon: 'error', title: 'Número de Licencia inválido', text: 'El número de licencia solo puede contener números', confirmButtonColor: '#dc3545'
});
@@ -175,9 +186,22 @@
return;
}
}
// SI TODAS LAS VALIDACIONES PASAN, ENVIAR EL FORMULARIO
this.submit();
// VALIDACIÓN ASÍNCRONA DE DUPLICADO DE GAFETE
fetch(`/IMPORTADORES/choferes/validarNumeroGafete?numero_gafete=${encodeURIComponent(numero_gafete)}`)
.then(response => response.json())
.then(data => {
if (data.success && data.existe) {
Swal.fire({ icon: 'error', title: 'Número de Gafete duplicado', text: 'El número de gafete ya está en uso, por favor ingresa uno diferente.', confirmButtonColor: '#dc3545' });
document.getElementById('numero_gafete').focus();
} else {
// Si no existe, enviamos el formulario
e.target.submit();
}
})
.catch(error => {
console.error('Error validando el número de gafete:', error);
Swal.fire({ icon: 'error', title: 'Error de validación', text: 'No fue posible validar el número de gafete. Intenta de nuevo.', confirmButtonColor: '#dc3545' });
});
});
</script>

View File

@@ -46,7 +46,7 @@
<?php foreach ($transportistas as $t): ?>
<option value="<?= $t['id_transportista'] ?>"
<?= $chofer['transportista_id'] == $t['id_transportista'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['nombre']) ?>
<?= htmlspecialchars($t['clave_identificador'] . ' - ' . $t['nombre'] . ' - ' . $t['ciudad_nombre'] . ' - ' . $t['domicilio']) ?>
</option>
<?php endforeach; ?>
</select>
@@ -76,6 +76,13 @@
value="<?= htmlspecialchars($chofer['numero_licencia']) ?>" required>
</div>
<!-- Gafete -->
<div class="col-md-6 mb-3">
<label for="numero_gafete" class="form-label">Número de Gafete</label>
<input name="numero_gafete" id="numero_gafete" type="text" maxlength="24" class="form-control"
value="<?= htmlspecialchars($chofer['numero_gafete']) ?>" required>
</div>
<!-- Teléfono -->
<div class="col-md-6 mb-3">
<label for="telefono" class="form-label">Teléfono</label>
@@ -138,6 +145,7 @@
const nombre = document.getElementById('nombre').value.trim();
const apellido = document.getElementById('apellido').value.trim();
const numero_licencia = document.getElementById('numero_licencia').value.trim();
const numero_gafete = document.getElementById('numero_gafete').value.trim();
const telefono = document.getElementById('telefono').value.trim();
const email = document.getElementById('email').value.trim();
const fecha_ingreso = document.getElementById('fecha_ingreso').value;
@@ -169,6 +177,11 @@
document.getElementById('numero_licencia').focus();
return;
}
if (!numero_gafete) {
Swal.fire({ icon: 'error', title: 'Número de Gafete requerido', text: 'Por favor ingresa el número de gafete.', confirmButtonColor: '#dc3545' });
document.getElementById('numero_gafete').focus();
return;
}
if (!soloNumerosRegex.test(numero_licencia)) {
Swal.fire({ icon: 'error', title: 'Número de Licencia inválido', text: 'El número de licencia solo puede contener números', confirmButtonColor: '#dc3545' });
document.getElementById('Clave').focus();
@@ -211,9 +224,24 @@
return;
}
}
// VALIDACIÓN ASÍNCRONA DE DUPLICADO DE GAFETE
const id_chofer = document.querySelector('input[name="id_chofer"]').value;
// SI TODAS LAS VALIDACIONES PASAN, ENVIAR EL FORMULARIO
this.submit();
fetch(`/IMPORTADORES/choferes/validarNumeroGafete?numero_gafete=${encodeURIComponent(numero_gafete)}&id_chofer={id_chofer}`)
.then(response => response.json())
.then(data => {
if (data.success && data.existe) {
Swal.fire({ icon: 'error', title: 'Número de Gafete duplicado', text: 'El número de gafete ya está en uso, por favor ingresa uno diferente.', confirmButtonColor: '#dc3545' });
document.getElementById('numero_gafete').focus();
} else {
// Si no existe, enviamos el formulario
e.target.submit();
}
})
.catch(error => {
console.error('Error validando el número de gafete:', error);
Swal.fire({ icon: 'error', title: 'Error de validación', text: 'No fue posible validar el número de gafete. Intenta de nuevo.', confirmButtonColor: '#dc3545' });
});
});
</script>

View File

@@ -47,6 +47,7 @@
<th>#</th>
<th>Nombre Completo</th>
<th>Licencia</th>
<th>Gafete</th>
<th>Teléfono</th>
<th>Email</th>
<th>Ingreso</th>
@@ -60,6 +61,7 @@
<td><?= $c['id_chofer'] ?></td>
<td><?= htmlspecialchars($c['nombre_completo']) ?></td>
<td><?= htmlspecialchars($c['numero_licencia']) ?></td>
<td><?= htmlspecialchars($c['numero_gafete']) ?></td>
<td><?= htmlspecialchars($c['telefono']) ?></td>
<td><?= htmlspecialchars($c['email']) ?></td>
<td>

View File

@@ -59,7 +59,7 @@
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-plus"></i> Registrar Estado
</button>
<a href="/IMPORTADORES/agentes/lista" class="btn btn-secondary ms-2">Cancelar</a>
<a href="/IMPORTADORES/locaciones/lista" class="btn btn-secondary ms-2">Cancelar</a>
</div>
</form>
@@ -69,7 +69,7 @@
<!-- Nueva Ciudad -->
<div class="col-md">
<div class="card p-4 bg-white shadow-sm">
<form id="ciudadForm" action="/IMPORTADORES/agentes/guadarCiudad" method="POST" enctype="multipart/form-data">
<form id="ciudadForm" action="/IMPORTADORES/locaciones/guadarCiudad" method="POST" enctype="multipart/form-data">
<h4 class="mb-4 text-dark"> Nueva Ciudad</h4>
<!-- País -->
<div class="col-md-12">
@@ -102,7 +102,7 @@
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-plus"></i> Registrar Ciudad
</button>
<a href="/IMPORTADORES/agentes/lista" class="btn btn-secondary ms-2">Cancelar</a>
<a href="/IMPORTADORES/locaciones/lista" class="btn btn-secondary ms-2">Cancelar</a>
</div>
</form>
</div>
@@ -117,7 +117,7 @@
selectElement.innerHTML = '<option>Cargando...</option>';
selectElement.disabled = true;
fetch(`/IMPORTADORES/agentes/estados?pais=${paisId}`)
fetch(`/IMPORTADORES/locaciones/estados?pais=${paisId}`)
.then(response => response.json())
.then(estados => {
selectElement.innerHTML = '<option value="">Selecciona estado</option>';
@@ -193,7 +193,7 @@
entidad: entidadValue
});
fetch('/IMPORTADORES/agentes/guardarEstado', {
fetch('/IMPORTADORES/locaciones/guardarEstado', {
method: 'POST',
body: formData
})
@@ -241,7 +241,7 @@
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Guardando...';
submitBtn.disabled = true;
fetch('/IMPORTADORES/agentes/guardarCiudad', {
fetch('/IMPORTADORES/locaciones/guardarCiudad', {
method: 'POST',
body: formData
})

View File

@@ -47,7 +47,7 @@
/* Asegurar que las pestañas sean completamente visibles */
.nav-tabs .nav-item:first-child .nav-link { margin-left: 0; }
.nav-tabs .nav-item:last-child .nav-link { margin-right: 0; }
.tab-content { padding: 5px; }
.tab-content { padding: 10px; }
</style>
</head>
<body>
@@ -119,7 +119,7 @@
<div class="col-md-10">
<input name="razon_social" id="razon_social" type="text" class="form-control" required>
</div>
</div><hr><br>
</div><hr>
<div class="col mb-3">
<P>Capturar para el llenado de la Manifestación de Valor</P>
@@ -251,7 +251,7 @@
</div>
<div style="display: flex; justify-content: right;">
<button type="submit" class="btn btn-success">Guardar</button>
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="/IMPORTADORES/patente/lista" class="btn btn-secondary ms-2">Cancelar</a>
</div>
</form>

View File

@@ -37,7 +37,7 @@
<body>
<div class="content">
<h4 class="mb-4">📦 Panel del Importador</h4>
<h4 class="mb-4">📦 Panel de Patentes</h4>
<div class="row g-4">
<div class="col-md-4">
@@ -45,8 +45,8 @@
<h5 class="text-primary">Ver patentes</h5>
<p>Revisa y gestiona las patentes.</p>
<a href="/IMPORTADORES/patente/lista"
class="btn btn-primary btn-sm mt-2 <?= $_SERVER['REQUEST_URI'] === '/IMPORTADORES/transportes/lista' ? 'active' : '' ?>">
Ver transportes
class="btn btn-primary btn-sm mt-2 <?= $_SERVER['REQUEST_URI'] === '/IMPORTADORES/transportes/lista' ? 'active' : '' ?>">
Ver patentes
</a>
</div>
</div>
@@ -56,8 +56,8 @@
<h5 class="text-success">Nueva patente</h5>
<p>Agrega nuevas patentes:</p>
<a href="/IMPORTADORES/patente/alta"
class="btn btn-success btn-sm mt-2 <?= $_SERVER['REQUEST_URI'] === '/IMPORTADORES/transportistas/lista' ? 'active' : '' ?>">
Ver transportistas
class="btn btn-success btn-sm mt-2 <?= $_SERVER['REQUEST_URI'] === '/IMPORTADORES/transportistas/lista' ? 'active' : '' ?>">
Registrar nueva patente
</a>
</div>
</div>

View File

@@ -44,7 +44,7 @@
/* Asegurar que las pestañas sean completamente visibles */
.nav-tabs .nav-item:first-child .nav-link { margin-left: 0; }
.nav-tabs .nav-item:last-child .nav-link { margin-right: 0; }
.tab-content { padding: 5px; }
.tab-content { padding: 10px; }
</style>
</head>
<body>
@@ -124,11 +124,11 @@
<input name="razon_social" id="razon_social" type="text" class="form-control"
value="<?= htmlspecialchars($agente['razon_social'] ?? '') ?>" required>
</div>
</div><hr><br>
</div><hr>
<div class="col mb-3">
<P>Capturar para el llenado de la Manifestación de Valor</P>
<p>Datos del Agente Aduanal:</p>
<p style="font-weight: bold;">Datos del Agente Aduanal:</p>
<div class="row mb-3">
<label for="mf_nombre" class="col-md-2 col-form-label">Nombre(s)</label>
<div class="col-md-9">
@@ -274,7 +274,7 @@
</div>
<div style="display: flex; justify-content: right;">
<button type="submit" class="btn btn-success">Guardar</button>
<button type="submit" class="btn btn-success">💾 Guardar</button>
<a href="/IMPORTADORES/patente/lista" class="btn btn-secondary ms-2">Cancelar</a>
</div>
</form>

View File

@@ -135,7 +135,7 @@
</select>
</div>
<div class="col-md-4 mb-3">
<label for="foto_solicitud" class="form-label">Foto de la solicitud</label>
<label for="foto_solicitud" class="form-label">Foto de la Carga (PIPA)</label>
<input id="foto_solicitud" name="foto_solicitud" type="file" class="form-control" accept="image/*">
</div>
</div>
@@ -217,135 +217,134 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
<script>
// 1) Inicializar Choices para todos los selects excepto proveedor_id
document.querySelectorAll('.searchable:not(#proveedor_id)').forEach(el => {
new Choices(el, { searchEnabled: true, itemSelectText: '', shouldSort: false });
document.addEventListener('DOMContentLoaded', () => {
// Primero cargamos proveedores, luego inicializamos Choices en todos los selects
cargarProveedores().then(() => {
inicializarChoicesGlobal();
});
// Inicializamos los eventos adicionales
inicializarEventos();
});
// 2) Cargar proveedores dinámicamente
const proveedorEl = document.getElementById('proveedor_id');
let proveedorChoices = null;
// Función para cargar proveedores (retorna promesa)
function cargarProveedores() {
const proveedorEl = document.getElementById('proveedor_id');
return fetch('/IMPORTADORES/solicitud_importacion/ajax_proveedores')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(json => {
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
json.results.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = item.text;
proveedorEl.appendChild(opt);
});
})
.catch(err => {
console.error('❌ Error cargando proveedores:', err);
proveedorEl.innerHTML = '<option value="">Error cargando proveedores</option>';
});
}
fetch('/IMPORTADORES/solicitud_importacion/ajax_proveedores')
.then(res => res.ok ? res.json() : Promise.reject(res.status))
.then(json => {
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
json.results.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.text = item.text;
proveedorEl.add(opt);
// Inicializamos Choices globalmente (todos los .searchable)
function inicializarChoicesGlobal() {
document.querySelectorAll('.searchable').forEach(el => {
if (el._choices) el._choices.destroy(); // destruye instancia previa si existe
el._choices = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false,
searchFields: ['label'] // 🔐 Solo busca en el texto visible
});
// destruir instancia previa si existe
if (proveedorChoices) proveedorChoices.destroy();
proveedorChoices = new Choices(proveedorEl, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
}
// Eventos principales del formulario
function inicializarEventos() {
// Agregar partidas dinámicamente
document.getElementById('add-partida').addEventListener('click', () => {
const tbody = document.querySelector('#tabla-partidas tbody');
const idx = tbody.querySelectorAll('tr').length;
const row = document.createElement('tr');
row.innerHTML = `
<td><input name="partidas[${idx}][descripcion]" class="form-control"></td>
<td><input name="partidas[${idx}][cantidad_comercial]" type="number" step="0.0001" class="form-control"></td>
<td><input name="partidas[${idx}][cantidad_tarifa]" type="number" step="0.0001" class="form-control"></td>
<td>
<select name="partidas[${idx}][unidad_comercial_id]" class="form-select searchable">
<option value="">-- Unidad --</option>
<?php foreach($unidades_medida as $um): ?>
<option value="<?= htmlspecialchars($um['id']) ?>"><?= htmlspecialchars($um['descripcion']) ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input name="partidas[${idx}][valor_factura]" type="number" step="0.01" class="form-control valor-partida"></td>
<td><input name="partidas[${idx}][peso_bruto]" type="number" step="0.0001" class="form-control"></td>
<td>
<select name="partidas[${idx}][tasa_preferencial]" class="form-select searchable">
<option value="">-- Selecciona --</option>
<option>General</option><option>TLC</option><option>PROSEC</option><option>ALADI</option><option>COMERCIALIZADORA</option>
</select>
</td>
<td class="hide"><input name="partidas[${idx}][precio_unitario]" type="number" class="form-control"></td>
<td class="hide"><input name="partidas[${idx}][oma_factura]" class="form-control"></td>
<td><button type="button" class="btn btn-danger btn-sm remove-row">✖️</button></td>
`;
tbody.appendChild(row);
// Inicializamos Choices en los nuevos selects
row.querySelectorAll('.searchable').forEach(el => {
el._choices = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
});
// seleccionar valor actual
const current = "<?= htmlspecialchars($factura['proveedor_id'], ENT_QUOTES) ?>";
if (current) {
proveedorChoices.setChoiceByValue(current);
});
// Eliminar partidas
document.querySelector('#tabla-partidas tbody').addEventListener('click', e => {
if (e.target.matches('.remove-row')) {
const row = e.target.closest('tr');
row.querySelectorAll('.searchable').forEach(el => {
if (el._choices) el._choices.destroy();
});
row.remove();
}
})
.catch(err => {
console.error('Error cargando proveedores:', err);
proveedorEl.innerHTML = '<option value="">No fue posible cargar proveedores</option>';
});
// 3) Agregar partida dinámica
document.getElementById('add-partida').addEventListener('click', () => {
const tbody = document.querySelector('#tabla-partidas tbody');
const idx = tbody.querySelectorAll('tr').length;
const row = document.createElement('tr');
row.innerHTML = `
<td><input name="partidas[\${idx}][descripcion]" class="form-control"></td>
<td><input name="partidas[\${idx}][cantidad_comercial]" type="number" step="0.0001" class="form-control"></td>
<td><input name="partidas[\${idx}][cantidad_tarifa]" type="number" step="0.0001" class="form-control"></td>
<td>
<select name="partidas[\${idx}][unidad_comercial_id]" class="form-select searchable">
<option value="">-- Unidad --</option>
<?php foreach($unidades_medida as $um): ?>
<option value="<?= htmlspecialchars($um['id']) ?>"><?= htmlspecialchars($um['descripcion']) ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input name="partidas[\${idx}][valor_factura]" type="number" step="0.01" class="form-control valor-partida"></td>
<td><input name="partidas[\${idx}][peso_bruto]" type="number" step="0.0001" class="form-control"></td>
<td>
<select name="partidas[\${idx}][tasa_preferencial]" class="form-select searchable">
<option value="">-- Selecciona --</option>
<option>General</option><option>TLC</option><option>PROSEC</option><option>ALADI</option><option>COMERCIALIZADORA</option>
</select>
</td>
<td class="hide"><input name="partidas[\${idx}][precio_unitario]" type="number" class="form-control"></td>
<td class="hide"><input name="partidas[\${idx}][oma_factura]" class="form-control"></td>
<td><button type="button" class="btn btn-danger btn-sm remove-row">✖️</button></td>
`;
tbody.appendChild(row);
// Re-inicializar Choices.js en los nuevos selects
row.querySelectorAll('.searchable').forEach(el => {
new Choices(el, { searchEnabled: true, itemSelectText: '', shouldSort: false });
// Control overflow tabla cuando se abre el dropdown
document.addEventListener('click', function(e) {
const tableContainer = document.querySelector('.table-responsive');
if (!tableContainer) return;
if (e.target.closest('.choices__inner')) {
tableContainer.style.overflow = 'visible';
} else {
tableContainer.style.overflow = 'auto';
}
});
});
// Manejador para controlar el overflow al abrir dropdowns
document.addEventListener('click', function(e) {
const tableContainer = document.querySelector('.table-responsive');
if (!tableContainer) return;
if (e.target.closest('.choices__inner')) {
tableContainer.style.overflow = 'visible';
} else {
tableContainer.style.overflow = 'auto';
}
});
document.querySelector('#tabla-partidas tbody').addEventListener('click', e => {
if (e.target.matches('.remove-row')) e.target.closest('tr').remove();
});
// validate suma partidas == valor_factura
$('#solicitudForm').submit(function(e){
const total = parseFloat($('#valor_factura').val())||0;
let sum = 0;
$('.valor-partida').each(function(){ sum += parseFloat($(this).val())||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)}).`
});
}
});
</script>
<script>
// proveedores script (no modificado)
document.querySelectorAll('.searchable').forEach(el => {
if (el.id !== 'proveedor_id') {
new Choices(el, { searchEnabled: true, itemSelectText: '', shouldSort: false });
}
});
const proveedorEl = document.getElementById('proveedor_id');
fetch('/IMPORTADORES/solicitud_importacion/ajax_proveedores')
.then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); })
.then(json => {
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
json.results.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id; opt.text = item.text;
proveedorEl.add(opt);
});
if (proveedorEl._choice) proveedorEl._choice.destroy();
new Choices(proveedorEl, { searchEnabled: true, itemSelectText: '', shouldSort: false });
})
.catch(err => {
console.error('Error cargando proveedores:', err);
proveedorEl.innerHTML = '<option value="">No fue posible cargar proveedores</option>';
// Validación suma de partidas al enviar
$('#solicitudForm').submit(function(e){
const total = parseFloat($('#valor_factura').val()) || 0;
let sum = 0;
$('.valor-partida').each(function(){ sum += parseFloat($(this).val()) || 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)}).`
});
}
});
}
</script>
</body>

View File

@@ -145,7 +145,7 @@
</select>
</div>
<div class="col-md-4 mb-3">
<label for="foto_solicitud" class="form-label">Foto de la solicitud</label>
<label for="foto_solicitud" class="form-label">Foto de la Carga (PIPA)</label>
<input id="foto_solicitud" name="foto_solicitud" type="file" class="form-control" accept="image/*">
</div>
</div>
@@ -243,7 +243,6 @@
</div>
</div>
<!-- Choices.js & jQuery -->
<!-- Choices.js JS -->
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

View File

@@ -29,6 +29,13 @@
.sidebar .nav-link.active { background-color: #e9ecef; color: #212529; }
}
.card { border-radius: 12px; }
/* Forzar z-index más alto para modales anidados */
.modal { z-index: 9999 !important; }
.modal-backdrop { z-index: 9998 !important; }
/* Asegurar que el contenido del modal esté por encima */
.modal-dialog { z-index: 10000 !important; position: relative; }
/* Opcional: Mejorar la apariencia del overlay */
.modal-backdrop.show { opacity: 0.5; }
</style>
</head>
<body>
@@ -39,12 +46,20 @@
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Contenedor *</label>
<input name="vehiculo" id="vehiculo" class="form-control" required>
<div class="d-flex align-items-center">
<input name="vehiculo" id="vehiculo" class="form-control" maxlength="20" required>
<button type="button" class="btn btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#infoContenedor" style="border: none;"></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Identificador fiscal *</label>
<input name="identificador_fiscal" id="identFiscal" class="form-control" required>
<label class="form-label">Identificación fiscal *</label>
<div class="d-flex align-items-center">
<input name="identificador_fiscal" id="ident_fiscal" class="form-control" maxlength="20" required>
<button type="button" class="btn btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#infoIdentificacion" style="border: none;"></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Foto (opcional)</label>
<input type="file" name="foto" id="foto" class="form-control" accept="image/*">
@@ -55,7 +70,7 @@
<option value="">Selecciona...</option>
<?php foreach($transportistas as $tr): ?>
<option value="<?= $tr['id_transportista'] ?>">
<?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'])) ?>
<?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'] . ' - ' . $tr['ciudad_nombre'] . ' - ' . $tr['domicilio'])) ?>
</option>
<?php endforeach; ?>
</select>
@@ -68,38 +83,70 @@
</form>
</div>
<!-- Modales movidos FUERA del formulario -->
<!-- Modal de ayuda - Contenedor -->
<div class="modal fade" id="infoContenedor" tabindex="-1" aria-labelledby="infoContenedorLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoContenedorLabel">Info - Contenedor</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<strong>• Número económico del vehículo.</strong>
</div>
</div>
</div>
</div>
<!-- Modal de ayuda - Identificación Fiscal -->
<div class="modal fade" id="infoIdentificacion" tabindex="-1" aria-labelledby="infoIdentificacionLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoIdentificacionLabel">Info - Identificación Fiscal</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
• Si el medio de transporte es <strong>vehículo terrestre</strong>, se anotarán las <strong>placas de circulación</strong> del mismo.<br><br>
• Si el medio de transporte es <strong>ferrocarril</strong>, se anotará el <strong>número de furgón o plataforma</strong>.<br><br>
• Si el medio de transporte es <strong>marítimo</strong>, se anotará el <strong>nombre de la embarcación</strong>.
</div>
</div>
</div>
</div>
<script>
document.getElementById('formTransCrear').addEventListener('submit', function(e) {
e.preventDefault(); // Prevenir envío por defecto
// Campos que validamos:
const veh = document.getElementById('vehiculo').value.trim();
const fisc = document.getElementById('identFiscal').value.trim();
const trans = document.getElementById('transportista').value;
const fotoF = document.getElementById('foto').files[0];
const vehiculo = document.getElementById('vehiculo').value.trim();
const ident_fiscal = document.getElementById('ident_fiscal').value.trim();
const transportista = document.getElementById('transportista').value;
const fotoF = document.getElementById('foto').files[0];
// Validaciones de campos obligatorios
if (!veh) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Vehículo requerido', text:'Por favor ingresa el nombre del vehículo.', confirmButtonColor: '#dc3545'});
document.querySelector('input[name="veh"]').focus();
if (!vehiculo) {
Swal.fire({ icon:'error', title:'Contenedor requerido', text:'Por favor ingresa el número económico del vehículo.', confirmButtonColor: '#dc3545'});
document.getElementById('vehiculo').focus();
return;
}
if (!fisc) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Identificador fiscal requerido', text:'Por favor ingresa el identificador fiscal.', confirmButtonColor: '#dc3545' });
document.querySelector('input[name="fisc"]').focus();
if (!ident_fiscal) {
Swal.fire({ icon:'error', title:'Identificación fiscal requerida', text:'Por favor ingresa la identificación fiscal.', confirmButtonColor: '#dc3545' });
document.getElementById('ident_fiscal').focus();
return;
}
if (!trans) {
e.preventDefault();
if (!transportista) {
Swal.fire({ icon:'error', title:'Transportista no seleccionado', text:'Debes elegir un transportista.', confirmButtonColor: '#dc3545' });
document.querySelector('input[name="trans"]').focus();
document.getElementById('transportista').focus();
return;
}
if (fotoF && fotoF.size > 2 * 1024 * 1024) { // 2 MB
e.preventDefault();
return Swal.fire({ icon:'error', title:'Foto demasiado grande', text:'La imagen no debe exceder 2 MB.' });
}
// Si todas las validaciones pasan, el formulario se envía.
this.submit();
});
</script>

View File

@@ -41,13 +41,19 @@
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Contenedor *</label>
<input name="vehiculo" id="vehEdit" class="form-control"
value="<?= htmlspecialchars($t['vehiculo']) ?>" required>
<div class="d-flex align-items-center">
<input name="vehiculo" id="vehiculo" class="form-control" maxlength="20"
value="<?= htmlspecialchars($t['vehiculo']) ?>" required>
<button type="button" class="btn btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#infoContenedor" style="border: none;"></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Identificador fiscal *</label>
<input name="identificador_fiscal" id="fiscEdit" class="form-control"
value="<?= htmlspecialchars($t['identificador_fiscal']) ?>" required>
<label class="form-label">Identificación fiscal *</label>
<div class="d-flex align-items-center">
<input name="identificador_fiscal" id="identificador_fiscal" class="form-control" maxlength="20"
value="<?= htmlspecialchars($t['identificador_fiscal']) ?>" required>
<button type="button" class="btn btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#infoIdentificacion" style="border: none;"></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Foto actual</label><br>
@@ -59,15 +65,15 @@
</div>
<div class="col-md-6">
<label class="form-label">Reemplazar foto (opcional)</label>
<input type="file" name="foto" id="fotoEdit" class="form-control" accept="image/*">
<input type="file" name="foto" id="foto" class="form-control" accept="image/*">
</div>
<div class="col-md-6">
<label class="form-label">Transportista *</label>
<select name="id_transportista" id="trEdit" class="form-select" required>
<select name="id_transportista" id="transportista" class="form-select" required>
<?php foreach($transportistas as $tr): ?>
<option value="<?= $tr['id_transportista'] ?>"
<?= $tr['id_transportista']==$t['id_transportista']?'selected':'' ?>>
<?= htmlspecialchars($tr['nombre']) ?>
<?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'] . ' - ' . $tr['ciudad_nombre'] . ' - ' . $tr['domicilio'])) ?>
</option>
<?php endforeach; ?>
</select>
@@ -80,38 +86,70 @@
</form>
</div>
<!-- Modales movidos FUERA del formulario -->
<!-- Modal de ayuda - Contenedor -->
<div class="modal fade" id="infoContenedor" tabindex="-1" aria-labelledby="infoContenedorLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoContenedorLabel">Info - Contenedor</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<strong>• Número económico del vehículo.</strong>
</div>
</div>
</div>
</div>
<!-- Modal de ayuda - Identificación Fiscal -->
<div class="modal fade" id="infoIdentificacion" tabindex="-1" aria-labelledby="infoIdentificacionLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoIdentificacionLabel">Info - Identificación Fiscal</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
• Si el medio de transporte es <strong>vehículo terrestre</strong>, se anotarán las <strong>placas de circulación</strong> del mismo.<br><br>
• Si el medio de transporte es <strong>ferrocarril</strong>, se anotará el <strong>número de furgón o plataforma</strong>.<br><br>
• Si el medio de transporte es <strong>marítimo</strong>, se anotará el <strong>nombre de la embarcación</strong>.
</div>
</div>
</div>
</div>
<script>
document.getElementById('formTransEdit').addEventListener('submit', function(e) {
e.preventDefault(); // Prevenir envío por defecto
// Campos que validamos:
const veh = document.getElementById('vehEdit').value.trim();
const fisc = document.getElementById('fiscEdit').value.trim();
const trans = document.getElementById('trEdit').value;
const fotoF = document.getElementById('fotoEdit').files[0];
const veh = document.getElementById('vehiculo').value.trim();
const fisc = document.getElementById('identificador_fiscal').value.trim();
const trans = document.getElementById('transportista').value;
const fotoF = document.getElementById('foto').files[0];
// Validaciones de campos obligatorios
if (!veh) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Vehículo requerido', text:'Por favor ingresa el nombre del vehículo.', confirmButtonColor: '#dc3545'});
document.querySelector('input[name="veh"]').focus();
Swal.fire({ icon:'error', title:'Contenedor requerido', text:'Por favor ingresa el número económico del vehículo.', confirmButtonColor: '#dc3545'});
document.getElementById('vehiculo').focus();
return;
}
if (!fisc) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Identificador fiscal requerido', text:'Por favor ingresa el identificador fiscal.', confirmButtonColor: '#dc3545' });
document.querySelector('input[name="fisc"]').focus();
Swal.fire({ icon:'error', title:'Identificación fiscal requerida', text:'Por favor ingresa la identificación fiscal.', confirmButtonColor: '#dc3545' });
document.getElementById('identificador_fiscal').focus();
return;
}
if (!trans) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Transportista no seleccionado', text:'Debes elegir un transportista.', confirmButtonColor: '#dc3545' });
document.querySelector('input[name="trans"]').focus();
document.getElementById('transportista').focus();
return;
}
if (fotoF && fotoF.size > 2 * 1024 * 1024) { // 2 MB
e.preventDefault();
return Swal.fire({ icon:'error', title:'Foto demasiado grande', text:'La imagen no debe exceder 2 MB.' });
}
// Si todas las validaciones pasan, el formulario se envía.
this.submit();
});
</script>

View File

@@ -57,7 +57,7 @@
<label class="form-label">Archivo CSV *</label>
<input type="file" name="csv" accept=".csv" class="form-control" required>
</div>
<p>Descarga la plantilla y llena las columnas: <code>contenedor, identificador_fiscal, id_transportista</code>.</p>
<p>Descarga la plantilla y llena las columnas: <code>contenedor, identificación_fiscal, id_transportista</code>.</p>
<a href="/IMPORTADORES/public/downloads/transportes_masivo_template.csv" class="btn btn-outline-secondary mb-3">
📥 Descargar plantilla
</a><br>

View File

@@ -48,7 +48,7 @@
<div class="col-md-4">
<label for="curp" class="form-label">CURP *</label>
<input name="curp" id="curp" class="form-control" maxlength="18" required>
<input name="curp" id="curp" class="form-control" maxlength="18">
</div>
<div class="col-md-4">
@@ -57,7 +57,7 @@
</div>
<div class="col-md-4">
<label for="caat" class="form-label">Código CAAT *</label>
<label for="caat" class="form-label">Código caat *</label>
<input name="caat" id="caat" class="form-control" maxlength="20" required>
</div>
@@ -89,7 +89,7 @@
<div class="col-md-8">
<label for="domicilio" class="form-label">Domicilio *</label>
<input name="domicilio" id="domicilio" class="form-control" required>
<input name="domicilio" id="domicilio" class="form-control" maxlength="100" required>
</div>
</div>
@@ -123,7 +123,6 @@
const curpRegex = /^[A-Z]{4}[0-9]{6}[HM](AS|BC|BS|CC|CL|CM|CS|CH|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS)[A-Z]{3}[A-Z0-9]{2}$/;
const rfcRegex = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
const telefonoRegex = /^\d{3}\s\d{7}$/;
const soloNumerosRegex = /^[0-9]+$/;
// Validaciones de campos obligatorios
if (!clave) {
@@ -151,11 +150,6 @@
document.getElementById('rfc').focus();
return;
}
if (!curp) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El CURP es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('curp').focus();
return;
}
// Validación de CURP (solo si se ingresó)
if (curp && !curpRegex.test(curp)) {
Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545'
@@ -179,12 +173,7 @@
return;
}
if (!caat) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código CAAT es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('caat').focus();
return;
}
if (!soloNumerosRegex.test(caat)) {
Swal.fire({ icon: 'error', title: 'Código CAAT inválido', text: 'El código caat solo puede contener números.', confirmButtonColor: '#dc3545' });
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código caat es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('caat').focus();
return;
}

View File

@@ -63,7 +63,7 @@
id="telefono" class="form-control" maxlength="11" required>
</div>
<div class="col-md-4">
<label class="form-label">Código CAAT</label>
<label class="form-label">Código caat</label>
<input name="caat" value="<?= htmlspecialchars($t['caat']) ?>"
id="caat" class="form-control" maxlength="20" required>
</div>
@@ -140,7 +140,6 @@
const curpRegex = /^[A-Z]{4}[0-9]{6}[HM](AS|BC|BS|CC|CL|CM|CS|CH|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS)[A-Z]{3}[A-Z0-9]{2}$/;
const rfcRegex = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
const telefonoRegex = /^\d{3}\s\d{7}$/;
const soloNumerosRegex = /^[0-9]+$/;
// Validaciones de campos obligatorios
if (!clave) {
@@ -168,11 +167,6 @@
document.getElementById('rfc').focus();
return;
}
if (!curp) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El CURP es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('curp').focus();
return;
}
// Validación de CURP (solo si se ingresó)
if (curp && !curpRegex.test(curp)) {
Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545'
@@ -196,12 +190,7 @@
return;
}
if (!caat) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código CAAT es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('caat').focus();
return;
}
if (!soloNumerosRegex.test(caat)) {
Swal.fire({ icon: 'error', title: 'Código CAAT inválido', text: 'El código caat solo puede contener números', confirmButtonColor: '#dc3545' });
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código caat es obligatorio.', confirmButtonColor: '#dc3545' });
document.getElementById('caat').focus();
return;
}

View File

@@ -67,29 +67,30 @@
$('#transportistas-table').DataTable({
serverSide: true,
processing: true,
searchDelay: 500, // espera 500 ms antes de lanzar la búsqueda
deferRender: true, // renderiza las filas sólo cuando tiene los datos
ajax: {
url: '/IMPORTADORES/transportistas/ajax_lista',
type: 'GET'
},
columns: [
{ data: 0 },
{ data: 1 },
{ data: 2 },
{ data: 3 },
{ data: 4 },
{ data: 5 },
{ data: 0 }, // ID
{ data: 1 }, // Código
{ data: 2 }, // Nombre
{ data: 3 }, // RFC
{ data: 4 }, // Ciudad
{ data: 5 }, // Fecha
{
data: null,
orderable: false,
searchable: false,
render: function(row) {
const id = row[0];
return `
render: function(data, type, row) {
const id = row[0];
return `
<a href="/IMPORTADORES/transportistas/editar?id=${id}" class="btn btn-sm btn-primary">✏️</a>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${id})">🗑️</button>
`;
`;
}
}
],
language: {