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

@@ -15,7 +15,7 @@ function lista() {
SELECT
c.*,
(c.nombre + ' ' + c.apellido) AS nombre_completo,
tr.nombre AS transportista
(tr.clave_identificador + ' - ' + tr.nombre) AS transportista
FROM dbo.choferes c
JOIN dbo.transportistas tr
ON c.transportista_id = tr.id_transportista
@@ -45,8 +45,10 @@ function crear() {
// OJO: aquí usamos "activo" según tu esquema original
$sql = "
SELECT id_transportista, nombre
FROM dbo.transportistas
SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
c.nombre AS ciudad_nombre
FROM dbo.transportistas t
LEFT JOIN dbo.ciudades c ON t.ciudad = c.id_ciudad
WHERE id_usuario = ? AND activo = 1
ORDER BY nombre
";
@@ -70,44 +72,86 @@ function guardar() {
$nombre = trim($_POST['nombre'] ?? '');
$apellido = trim($_POST['apellido'] ?? '');
$licencia = trim($_POST['numero_licencia'] ?? '');
$gafete = trim($_POST['numero_gafete'] ?? '');
$telefono = trim($_POST['telefono'] ?? '');
$email = trim($_POST['email'] ?? '');
$fecha_ingreso = $_POST['fecha_ingreso'] ?: null;
if (!$transportista_id || $nombre === '' || $apellido === '' || $licencia === '') {
if (!$transportista_id || !is_numeric($transportista_id) || $nombre === '' ||
$apellido === '' || $licencia === '' || $gafete === '') {
die("❌ Todos los campos obligatorios deben llenarse.");
}
// Manejo de foto
$conn = getConnection();
// ✅ CRÍTICO: Verificar que el transportista pertenece al usuario
$sqlVerify = "SELECT id_transportista FROM dbo.transportistas WHERE id_transportista = ? AND id_usuario = ?";
$stmtVerify = sqlsrv_query($conn, $sqlVerify, [(int)$transportista_id, $_SESSION['usuario_id']]);
if (!$stmtVerify || !sqlsrv_fetch($stmtVerify)) {
die("❌ Transportista no autorizado.");
}
// ✅ CRÍTICO: Verificar que el número de gafete no existe
$sqlCheckGafete = "SELECT id_chofer FROM dbo.choferes WHERE numero_gafete = ? AND status = 1";
$stmtCheck = sqlsrv_query($conn, $sqlCheckGafete, [$gafete]);
if ($stmtCheck && sqlsrv_fetch($stmtCheck)) {
die("❌ El número de gafete '{$gafete}' ya está en uso. Por favor, use otro número.");
}
// ✅ MEJORADO: Manejo de foto con validación
$fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!empty($_FILES['foto']['tmp_name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowedTypes = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedTypes)) {
die("❌ Tipo de archivo no permitido. Solo JPG, PNG, GIF.");
}
// Validar tamaño (ej: máximo 5MB)
if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {
die("❌ El archivo es demasiado grande. Máximo 5MB.");
}
$dest = __DIR__ . '/../../public/uploads/chofer_'.uniqid().".{$ext}";
if (!is_dir(dirname($dest))) mkdir(dirname($dest), 0755, true);
if (!is_dir(dirname($dest))) {
mkdir(dirname($dest), 0755, true);
}
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else {
error_log("Error al mover foto en guardar(): {$dest}");
die("❌ Error al subir la foto.");
}
}
$conn = getConnection();
$sql = "
INSERT INTO dbo.choferes
(transportista_id, nombre, apellido, numero_licencia, telefono, email, fecha_ingreso, foto_url, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
(transportista_id, nombre, apellido, numero_licencia, numero_gafete, telefono, email, fecha_ingreso, foto_url, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, GETDATE())
";
$params = [
(int)$transportista_id,
$nombre,
$apellido,
$licencia,
$gafete,
$telefono,
$email,
$fecha_ingreso,
$fotoUrl
];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
die("Error en guardar(): " . print_r(sqlsrv_errors(), true));
$errors = sqlsrv_errors();
// ✅ Manejo específico de error de duplicado
foreach ($errors as $error) {
if (strpos($error['message'], 'UQ_numero_gafete') !== false) {
die("❌ El número de gafete ya está en uso. Por favor, use otro número.");
}
}
die("❌ Error en guardar(): " . print_r($errors, true));
}
header('Location: /IMPORTADORES/choferes/lista?created=ok');
@@ -126,13 +170,15 @@ function editar() {
$conn = getConnection();
$sql = "
SELECT c.*, tr.nombre AS transportista
FROM dbo.choferes c
JOIN dbo.transportistas tr
ON c.transportista_id = tr.id_transportista
WHERE c.id_chofer = ?
SELECT
ch.*, tr.clave_identificador, tr.nombre AS transportista_nombre, tr.ciudad, tr.domicilio,
ciu.nombre AS ciudad_nombre
FROM dbo.choferes ch
LEFT JOIN dbo.transportistas tr ON ch.transportista_id = tr.id_transportista
LEFT JOIN dbo.ciudades ciu ON tr.ciudad = ciu.id_ciudad
WHERE ch.id_chofer = ?
AND tr.id_usuario = ?
AND c.status = 1
AND ch.status = 1
";
$stmt = sqlsrv_query($conn, $sql, [(int)$id, $_SESSION['usuario_id']]);
if ($stmt === false) {
@@ -150,10 +196,12 @@ function editar() {
// Lista de transportistas
$sql2 = "
SELECT id_transportista, nombre
FROM dbo.transportistas
WHERE id_usuario = ? AND activo = 1
ORDER BY nombre
SELECT tr.id_transportista, tr.clave_identificador, tr.nombre, tr.domicilio,
ciu.nombre AS ciudad_nombre
FROM dbo.transportistas tr
LEFT JOIN dbo.ciudades ciu ON tr.ciudad = ciu.id_ciudad
WHERE tr.id_usuario = ? AND tr.activo = 1
ORDER BY tr.nombre
";
$stmt2 = sqlsrv_query($conn, $sql2, [$_SESSION['usuario_id']]);
if ($stmt2 === false) {
@@ -167,6 +215,42 @@ function editar() {
include __DIR__ . '/../../views/choferes/editar.php';
}
/** Valida que el número de gafete sea único **/
function validarNumeroGafete() {
if (!($_SESSION['usuario_id'] ?? false)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'No autorizado']);
exit;
}
$gafete = trim($_GET['numero_gafete'] ?? '');
$id_chofer = $_GET['id_chofer'] ?? null;
if ($gafete === '') {
echo json_encode(['success' => false, 'message' => 'Número de gafete vacío']);
exit;
}
$conn = getConnection();
if ($id_chofer) {
// Edición: excluir el chofer actual
$sql = "SELECT id_chofer FROM dbo.choferes WHERE numero_gafete = ? AND id_chofer <> ? AND status = 1";
$stmt = sqlsrv_query($conn, $sql, [$gafete, $id_chofer]);
} else {
// Alta nueva
$sql = "SELECT id_chofer FROM dbo.choferes WHERE numero_gafete = ? AND status = 1";
$stmt = sqlsrv_query($conn, $sql, [$gafete]);
}
if ($stmt && sqlsrv_fetch($stmt)) {
echo json_encode(['success' => true, 'existe' => true]);
} else {
echo json_encode(['success' => true, 'existe' => false]);
}
exit;
}
/** Procesa la actualización de un chofer **/
function actualizar() {
if (!($_SESSION['usuario_id'] ?? false)) {
@@ -178,6 +262,7 @@ function actualizar() {
$nombre = trim($_POST['nombre'] ?? '');
$apellido = trim($_POST['apellido'] ?? '');
$licencia = trim($_POST['numero_licencia']?? '');
$gafete = trim($_POST['numero_gafete']?? '');
$telefono = trim($_POST['telefono'] ?? '');
$email = trim($_POST['email'] ?? '');
$fecha_ingreso = $_POST['fecha_ingreso'] ?: null;
@@ -187,15 +272,35 @@ function actualizar() {
if (
!$id || !is_numeric($id) ||
!$transportista_id || !is_numeric($transportista_id) ||
$nombre === '' || $apellido === '' || $licencia === ''
$nombre === '' || $apellido === '' || $licencia === '' || $gafete === ''
) {
die("❌ Datos inválidos o incompletos.");
}
// Manejo de foto nueva (opcional)
$conn = getConnection();
// ✅ CRÍTICO: Verificar que el número de gafete no existe (excluyendo el chofer actual)
$sqlCheckGafete = "SELECT id_chofer FROM dbo.choferes WHERE numero_gafete = ? AND id_chofer <> ? AND status = 1";
$stmtCheck = sqlsrv_query($conn, $sqlCheckGafete, [$gafete, $id_chofer]);
if ($stmtCheck && sqlsrv_fetch($stmtCheck)) {
die("❌ El número de gafete '{$gafete}' ya está en uso. Por favor, use otro número.");
}
// ✅ MEJORADO: Manejo de foto nueva con validación
$fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowedTypes = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedTypes)) {
die("❌ Tipo de archivo no permitido. Solo JPG, PNG, GIF.");
}
// Validar tamaño (ej: máximo 5MB)
if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {
die("❌ El archivo es demasiado grande. Máximo 5MB.");
}
$dest = __DIR__ . '/../../public/uploads/chofer_'.uniqid().".{$ext}";
if (!is_dir(dirname($dest))) {
mkdir(dirname($dest), 0755, true);
@@ -204,11 +309,10 @@ function actualizar() {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else {
error_log("Error al mover foto en actualizar(): {$dest}");
die("❌ Error al subir la foto.");
}
}
$conn = getConnection();
if ($fotoUrl) {
$sql = "
UPDATE dbo.choferes SET
@@ -216,6 +320,7 @@ function actualizar() {
nombre = ?,
apellido = ?,
numero_licencia = ?,
numero_gafete = ?,
telefono = ?,
email = ?,
fecha_ingreso = ?,
@@ -229,6 +334,7 @@ function actualizar() {
$nombre,
$apellido,
$licencia,
$gafete,
$telefono,
$email,
$fecha_ingreso,
@@ -243,6 +349,7 @@ function actualizar() {
nombre = ?,
apellido = ?,
numero_licencia = ?,
numero_gafete = ?,
telefono = ?,
email = ?,
fecha_ingreso = ?,
@@ -255,6 +362,7 @@ function actualizar() {
$nombre,
$apellido,
$licencia,
$gafete,
$telefono,
$email,
$fecha_ingreso,
@@ -265,7 +373,14 @@ function actualizar() {
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
die("❌ Error en actualizar(): " . print_r(sqlsrv_errors(), true));
$errors = sqlsrv_errors();
// ✅ Manejo específico de error de duplicado
foreach ($errors as $error) {
if (strpos($error['message'], 'UQ_numero_gafete') !== false) {
die("❌ El número de gafete ya está en uso por otro chofer. Por favor, use otro número.");
}
}
die("❌ Error en actualizar(): " . print_r($errors, true));
}
header('Location: /IMPORTADORES/choferes/lista?updated=ok');

View File

@@ -14,7 +14,7 @@ function lista() {
// Sólo mostrar transportes de los transportistas que le pertenecen al usuario
$sql = "
SELECT t.*, tr.nombre AS transportista
SELECT t.*, (tr.clave_identificador + ' - ' + tr.nombre) AS transportista
FROM dbo.transportes t
JOIN dbo.transportistas tr
ON t.id_transportista = tr.id_transportista
@@ -41,8 +41,10 @@ function crear() {
// Traer transportistas propios para el select
$sql = "
SELECT id_transportista, clave_identificador, nombre
FROM dbo.transportistas
SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
c.nombre AS ciudad_nombre
FROM dbo.transportistas t
LEFT JOIN dbo.ciudades c ON t.ciudad = c.id_ciudad
WHERE id_usuario = ? AND activo = 1
ORDER BY nombre
";
@@ -56,31 +58,62 @@ function crear() {
}
/** Procesa la creación de un nuevo transporte **/
function guardar() {
function guardar()
{
if (!($_SESSION['usuario_id'] ?? false)) {
die("⚠️ No autorizado.");
}
$vehiculo = trim($_POST['vehiculo'] ?? '');
$identFiscal= trim($_POST['identificador_fiscal'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null;
$vehiculo = trim($_POST['vehiculo'] ?? '');
$identFiscal = trim($_POST['identificador_fiscal'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null;
if ($vehiculo === '' || $identFiscal === '' || !$idTrans) {
die("❌ Todos los campos son obligatorios.");
}
// Manejo de foto
// Validar que el transportista pertenece al usuario actual
$conn = getConnection();
$sqlCheck = "
SELECT 1 FROM dbo.transportistas
WHERE id_transportista = ? AND id_usuario = ? AND activo = 1
";
$stmtCheck = sqlsrv_query($conn, $sqlCheck, [$idTrans, $_SESSION['usuario_id']]);
if (!sqlsrv_fetch($stmtCheck)) {
die("❌ Transportista no válido o no autorizado.");
}
// Manejo de foto mejorado
$fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name'])) {
$ext = pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION);
$dest = __DIR__ . '/../../public/uploads/transporte_'.uniqid().".{$ext}";
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) {
die("❌ Formato de imagen no válido. Solo se permiten: " . implode(', ', $allowedExtensions));
}
// Validar tamaño (2MB max)
if ($_FILES['foto']['size'] > 2 * 1024 * 1024) {
die("❌ La imagen no debe exceder 2 MB.");
}
$uploadDir = __DIR__ . '/../../public/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$dest = $uploadDir . 'transporte_' . uniqid() . ".{$ext}";
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
// ruta relativa
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else {
error_log("Error al mover archivo en guardar(): {$dest}");
die("❌ Error al subir la imagen.");
}
}
$conn = getConnection();
// Insertar el nuevo transporte
$sql = "
INSERT INTO dbo.transportes
(vehiculo, identificador_fiscal, foto_url, status, id_transportista)
@@ -88,8 +121,17 @@ function guardar() {
";
$params = [$vehiculo, $identFiscal, $fotoUrl, $idTrans];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
die("❌ Error al guardar: ".print_r(sqlsrv_errors(),true));
$errors = sqlsrv_errors();
error_log("Error SQL en guardar transporte: " . print_r($errors, true));
die("❌ Error al guardar: " . $errors[0]['message']);
}
// Verificar que se insertó correctamente
$rowsAffected = sqlsrv_rows_affected($stmt);
if ($rowsAffected === 0) {
die("❌ No se pudo crear el registro.");
}
header('Location: /IMPORTADORES/transportes/lista?created=ok');
@@ -124,8 +166,10 @@ function editar() {
// Mismo select de transportistas que en crear()
$sql2 = "
SELECT id_transportista, nombre
FROM dbo.transportistas
SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
c.nombre AS ciudad_nombre
FROM dbo.transportistas t
LEFT JOIN dbo.ciudades c ON t.ciudad = c.id_ciudad
WHERE id_usuario = ? AND activo = 1
ORDER BY nombre
";
@@ -145,24 +189,49 @@ function actualizar() {
$vehiculo = trim($_POST['vehiculo'] ?? '');
$identFiscal = trim($_POST['identificador_fiscal'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null;
if (!$id || !is_numeric($id) || $vehiculo === '' || $identFiscal === '' || !$idTrans) {
die("❌ Faltan datos.");
}
$conn = getConnection();
// Antes del UPDATE, validar que el transporte pertenece al usuario
$sqlCheck = "
SELECT 1 FROM dbo.transportes t
JOIN dbo.transportistas tr ON t.id_transportista = tr.id_transportista
wHERE t.id_transporte = ? AND tr.id_usuario = ?
";
$stmtCheck = sqlsrv_query($conn, $sqlCheck, [$id, $_SESSION['usuario_id']]);
if ($stmtCheck === false) {
$errors = sqlsrv_errors();
error_log("Error SQL al validar transporte: " . print_r($errors, true));
die("❌ Error en la validación SQL.");
}
if (!sqlsrv_fetch($stmtCheck)) {
die("❌ No autorizado para modificar este transporte.");
}
// —– Manejo de nueva foto —–
$fotoUrl = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) {
die("❌ Formato de imagen no válido.");
}
$dest = __DIR__ . '/../../public/uploads/transporte_'.uniqid().".{$ext}";
if (!is_dir(dirname($dest))) {
mkdir(dirname($dest), 0755, true);
}
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else {
error_log("Error al mover archivo en actualizar(): {$dest}");
die("❌ Error al subir la imagen.");
}
}
@@ -174,7 +243,7 @@ function actualizar() {
identificador_fiscal = ?,
id_transportista = ?,
foto_url = ?
WHERE id_transporte = ?
WHERE id_transporte = ?
";
$params = [$vehiculo, $identFiscal, $idTrans, $fotoUrl, $id];
} else {
@@ -190,7 +259,15 @@ function actualizar() {
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
die("❌ Error al actualizar: " . print_r(sqlsrv_errors(), true));
$errors = sqlsrv_errors();
error_log("Error SQL en actualizar transporte: " . print_r($errors, true));
die("❌ Error al actualizar: " . $errors[0]['message']);
}
// Verificar si se afectó alguna fila
$rowsAffected = sqlsrv_rows_affected($stmt);
if ($rowsAffected === 0) {
die("❌ No se pudo actualizar el registro.");
}
header('Location: /IMPORTADORES/transportes/lista?updated=ok');

View File

@@ -2,6 +2,7 @@
require_once __DIR__ . '/../helpers/session.php';
require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../helpers/env.php';
ob_clean();
function guardar() {
if (!($_SESSION['usuario_id'] ?? false)) {
@@ -264,21 +265,13 @@ function ajax_lista() {
$params = [$usr];
if ($search !== '') {
// Ahora buscamos en campos de transportista Y en el nombre de la ciudad
$where .= " AND (
t.clave_identificador LIKE ? OR
t.nombre LIKE ? OR
t.rfc LIKE ? OR
t.curp LIKE ? OR
t.telefono LIKE ? OR
t.caat LIKE ? OR
c.nombre LIKE ? OR
p.nombre LIKE ? OR
e.nombre LIKE ?
c.nombre LIKE ?
)";
$like = "%{$search}%";
// Agregamos el parámetro para cada campo de búsqueda
$params = array_merge($params, array_fill(0, 9, $like));
$params = array_merge($params, array_fill(0, 3, $like));
}
// 5) Total registros filtrados (CON JOIN)
@@ -332,13 +325,23 @@ function ajax_lista() {
}
// 7) Devolver JSON
$response = [
"draw" => $draw,
"recordsTotal" => $recordsTotal,
"recordsFiltered" => $recordsFiltered,
"data" => $data
];
$json = json_encode($response, JSON_UNESCAPED_UNICODE);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(500);
echo json_encode(["error" => "JSON encoding error: " . json_last_error_msg()]);
exit;
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
"draw" => $draw,
"recordsTotal" => $recordsTotal,
"recordsFiltered" => $recordsFiltered,
"data" => $data
]);
header('Cache-Control: no-cache, must-revalidate');
echo $json;
exit;
}

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');
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);
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>';
});
// destruir instancia previa si existe
if (proveedorChoices) proveedorChoices.destroy();
proveedorChoices = new Choices(proveedorEl, {
}
// 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
shouldSort: false,
searchFields: ['label'] // 🔐 Solo busca en el texto visible
});
// seleccionar valor actual
const current = "<?= htmlspecialchars($factura['proveedor_id'], ENT_QUOTES) ?>";
if (current) {
proveedorChoices.setChoiceByValue(current);
});
}
// 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
});
});
});
// 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: {