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 SELECT
c.*, c.*,
(c.nombre + ' ' + c.apellido) AS nombre_completo, (c.nombre + ' ' + c.apellido) AS nombre_completo,
tr.nombre AS transportista (tr.clave_identificador + ' - ' + tr.nombre) AS transportista
FROM dbo.choferes c FROM dbo.choferes c
JOIN dbo.transportistas tr JOIN dbo.transportistas tr
ON c.transportista_id = tr.id_transportista ON c.transportista_id = tr.id_transportista
@@ -45,8 +45,10 @@ function crear() {
// OJO: aquí usamos "activo" según tu esquema original // OJO: aquí usamos "activo" según tu esquema original
$sql = " $sql = "
SELECT id_transportista, nombre SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
FROM dbo.transportistas 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 WHERE id_usuario = ? AND activo = 1
ORDER BY nombre ORDER BY nombre
"; ";
@@ -70,44 +72,86 @@ function guardar() {
$nombre = trim($_POST['nombre'] ?? ''); $nombre = trim($_POST['nombre'] ?? '');
$apellido = trim($_POST['apellido'] ?? ''); $apellido = trim($_POST['apellido'] ?? '');
$licencia = trim($_POST['numero_licencia'] ?? ''); $licencia = trim($_POST['numero_licencia'] ?? '');
$gafete = trim($_POST['numero_gafete'] ?? '');
$telefono = trim($_POST['telefono'] ?? ''); $telefono = trim($_POST['telefono'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
$fecha_ingreso = $_POST['fecha_ingreso'] ?: null; $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."); 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; $fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name'])) { if (!empty($_FILES['foto']['tmp_name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION)); $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}"; $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)) { if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($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 = " $sql = "
INSERT INTO dbo.choferes INSERT INTO dbo.choferes
(transportista_id, nombre, apellido, numero_licencia, telefono, email, fecha_ingreso, foto_url, status) (transportista_id, nombre, apellido, numero_licencia, numero_gafete, telefono, email, fecha_ingreso, foto_url, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, GETDATE())
"; ";
$params = [ $params = [
(int)$transportista_id, (int)$transportista_id,
$nombre, $nombre,
$apellido, $apellido,
$licencia, $licencia,
$gafete,
$telefono, $telefono,
$email, $email,
$fecha_ingreso, $fecha_ingreso,
$fotoUrl $fotoUrl
]; ];
$stmt = sqlsrv_query($conn, $sql, $params); $stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) { 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'); header('Location: /IMPORTADORES/choferes/lista?created=ok');
@@ -126,13 +170,15 @@ function editar() {
$conn = getConnection(); $conn = getConnection();
$sql = " $sql = "
SELECT c.*, tr.nombre AS transportista SELECT
FROM dbo.choferes c ch.*, tr.clave_identificador, tr.nombre AS transportista_nombre, tr.ciudad, tr.domicilio,
JOIN dbo.transportistas tr ciu.nombre AS ciudad_nombre
ON c.transportista_id = tr.id_transportista FROM dbo.choferes ch
WHERE c.id_chofer = ? LEFT JOIN dbo.transportistas tr ON ch.transportista_id = tr.id_transportista
AND tr.id_usuario = ? LEFT JOIN dbo.ciudades ciu ON tr.ciudad = ciu.id_ciudad
AND c.status = 1 WHERE ch.id_chofer = ?
AND tr.id_usuario = ?
AND ch.status = 1
"; ";
$stmt = sqlsrv_query($conn, $sql, [(int)$id, $_SESSION['usuario_id']]); $stmt = sqlsrv_query($conn, $sql, [(int)$id, $_SESSION['usuario_id']]);
if ($stmt === false) { if ($stmt === false) {
@@ -150,10 +196,12 @@ function editar() {
// Lista de transportistas // Lista de transportistas
$sql2 = " $sql2 = "
SELECT id_transportista, nombre SELECT tr.id_transportista, tr.clave_identificador, tr.nombre, tr.domicilio,
FROM dbo.transportistas ciu.nombre AS ciudad_nombre
WHERE id_usuario = ? AND activo = 1 FROM dbo.transportistas tr
ORDER BY nombre 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']]); $stmt2 = sqlsrv_query($conn, $sql2, [$_SESSION['usuario_id']]);
if ($stmt2 === false) { if ($stmt2 === false) {
@@ -167,6 +215,42 @@ function editar() {
include __DIR__ . '/../../views/choferes/editar.php'; 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 **/ /** Procesa la actualización de un chofer **/
function actualizar() { function actualizar() {
if (!($_SESSION['usuario_id'] ?? false)) { if (!($_SESSION['usuario_id'] ?? false)) {
@@ -178,6 +262,7 @@ function actualizar() {
$nombre = trim($_POST['nombre'] ?? ''); $nombre = trim($_POST['nombre'] ?? '');
$apellido = trim($_POST['apellido'] ?? ''); $apellido = trim($_POST['apellido'] ?? '');
$licencia = trim($_POST['numero_licencia']?? ''); $licencia = trim($_POST['numero_licencia']?? '');
$gafete = trim($_POST['numero_gafete']?? '');
$telefono = trim($_POST['telefono'] ?? ''); $telefono = trim($_POST['telefono'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
$fecha_ingreso = $_POST['fecha_ingreso'] ?: null; $fecha_ingreso = $_POST['fecha_ingreso'] ?: null;
@@ -187,15 +272,35 @@ function actualizar() {
if ( if (
!$id || !is_numeric($id) || !$id || !is_numeric($id) ||
!$transportista_id || !is_numeric($transportista_id) || !$transportista_id || !is_numeric($transportista_id) ||
$nombre === '' || $apellido === '' || $licencia === '' $nombre === '' || $apellido === '' || $licencia === '' || $gafete === ''
) { ) {
die("❌ Datos inválidos o incompletos."); 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; $fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) { 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)); $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}"; $dest = __DIR__ . '/../../public/uploads/chofer_'.uniqid().".{$ext}";
if (!is_dir(dirname($dest))) { if (!is_dir(dirname($dest))) {
mkdir(dirname($dest), 0755, true); mkdir(dirname($dest), 0755, true);
@@ -204,11 +309,10 @@ function actualizar() {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest); $fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else { } else {
error_log("Error al mover foto en actualizar(): {$dest}"); error_log("Error al mover foto en actualizar(): {$dest}");
die("❌ Error al subir la foto.");
} }
} }
$conn = getConnection();
if ($fotoUrl) { if ($fotoUrl) {
$sql = " $sql = "
UPDATE dbo.choferes SET UPDATE dbo.choferes SET
@@ -216,6 +320,7 @@ function actualizar() {
nombre = ?, nombre = ?,
apellido = ?, apellido = ?,
numero_licencia = ?, numero_licencia = ?,
numero_gafete = ?,
telefono = ?, telefono = ?,
email = ?, email = ?,
fecha_ingreso = ?, fecha_ingreso = ?,
@@ -229,6 +334,7 @@ function actualizar() {
$nombre, $nombre,
$apellido, $apellido,
$licencia, $licencia,
$gafete,
$telefono, $telefono,
$email, $email,
$fecha_ingreso, $fecha_ingreso,
@@ -243,6 +349,7 @@ function actualizar() {
nombre = ?, nombre = ?,
apellido = ?, apellido = ?,
numero_licencia = ?, numero_licencia = ?,
numero_gafete = ?,
telefono = ?, telefono = ?,
email = ?, email = ?,
fecha_ingreso = ?, fecha_ingreso = ?,
@@ -255,6 +362,7 @@ function actualizar() {
$nombre, $nombre,
$apellido, $apellido,
$licencia, $licencia,
$gafete,
$telefono, $telefono,
$email, $email,
$fecha_ingreso, $fecha_ingreso,
@@ -265,7 +373,14 @@ function actualizar() {
$stmt = sqlsrv_query($conn, $sql, $params); $stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) { 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'); 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 // Sólo mostrar transportes de los transportistas que le pertenecen al usuario
$sql = " $sql = "
SELECT t.*, tr.nombre AS transportista SELECT t.*, (tr.clave_identificador + ' - ' + tr.nombre) AS transportista
FROM dbo.transportes t FROM dbo.transportes t
JOIN dbo.transportistas tr JOIN dbo.transportistas tr
ON t.id_transportista = tr.id_transportista ON t.id_transportista = tr.id_transportista
@@ -41,8 +41,10 @@ function crear() {
// Traer transportistas propios para el select // Traer transportistas propios para el select
$sql = " $sql = "
SELECT id_transportista, clave_identificador, nombre SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
FROM dbo.transportistas 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 WHERE id_usuario = ? AND activo = 1
ORDER BY nombre ORDER BY nombre
"; ";
@@ -56,31 +58,62 @@ function crear() {
} }
/** Procesa la creación de un nuevo transporte **/ /** Procesa la creación de un nuevo transporte **/
function guardar() { function guardar()
{
if (!($_SESSION['usuario_id'] ?? false)) { if (!($_SESSION['usuario_id'] ?? false)) {
die("⚠️ No autorizado."); die("⚠️ No autorizado.");
} }
$vehiculo = trim($_POST['vehiculo'] ?? '');
$identFiscal= trim($_POST['identificador_fiscal'] ?? ''); $vehiculo = trim($_POST['vehiculo'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null; $identFiscal = trim($_POST['identificador_fiscal'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null;
if ($vehiculo === '' || $identFiscal === '' || !$idTrans) { if ($vehiculo === '' || $identFiscal === '' || !$idTrans) {
die("❌ Todos los campos son obligatorios."); 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; $fotoUrl = null;
if (!empty($_FILES['foto']['tmp_name'])) { if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION); $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$dest = __DIR__ . '/../../public/uploads/transporte_'.uniqid().".{$ext}"; $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)) { if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
// ruta relativa
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest); $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 = " $sql = "
INSERT INTO dbo.transportes INSERT INTO dbo.transportes
(vehiculo, identificador_fiscal, foto_url, status, id_transportista) (vehiculo, identificador_fiscal, foto_url, status, id_transportista)
@@ -88,8 +121,17 @@ function guardar() {
"; ";
$params = [$vehiculo, $identFiscal, $fotoUrl, $idTrans]; $params = [$vehiculo, $identFiscal, $fotoUrl, $idTrans];
$stmt = sqlsrv_query($conn, $sql, $params); $stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) { 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'); header('Location: /IMPORTADORES/transportes/lista?created=ok');
@@ -124,8 +166,10 @@ function editar() {
// Mismo select de transportistas que en crear() // Mismo select de transportistas que en crear()
$sql2 = " $sql2 = "
SELECT id_transportista, nombre SELECT t.id_transportista, t.clave_identificador, t.nombre, t.ciudad, t.domicilio,
FROM dbo.transportistas 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 WHERE id_usuario = ? AND activo = 1
ORDER BY nombre ORDER BY nombre
"; ";
@@ -145,24 +189,49 @@ function actualizar() {
$vehiculo = trim($_POST['vehiculo'] ?? ''); $vehiculo = trim($_POST['vehiculo'] ?? '');
$identFiscal = trim($_POST['identificador_fiscal'] ?? ''); $identFiscal = trim($_POST['identificador_fiscal'] ?? '');
$idTrans = $_POST['id_transportista'] ?? null; $idTrans = $_POST['id_transportista'] ?? null;
if (!$id || !is_numeric($id) || $vehiculo === '' || $identFiscal === '' || !$idTrans) { if (!$id || !is_numeric($id) || $vehiculo === '' || $identFiscal === '' || !$idTrans) {
die("❌ Faltan datos."); die("❌ Faltan datos.");
} }
$conn = getConnection(); $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 —– // —– Manejo de nueva foto —–
$fotoUrl = null; $fotoUrl = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) { if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION)); $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}"; $dest = __DIR__ . '/../../public/uploads/transporte_'.uniqid().".{$ext}";
if (!is_dir(dirname($dest))) { if (!is_dir(dirname($dest))) {
mkdir(dirname($dest), 0755, true); mkdir(dirname($dest), 0755, true);
} }
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) { if (move_uploaded_file($_FILES['foto']['tmp_name'], $dest)) {
$fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest); $fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest);
} else { } else {
error_log("Error al mover archivo en actualizar(): {$dest}"); error_log("Error al mover archivo en actualizar(): {$dest}");
die("❌ Error al subir la imagen.");
} }
} }
@@ -174,7 +243,7 @@ function actualizar() {
identificador_fiscal = ?, identificador_fiscal = ?,
id_transportista = ?, id_transportista = ?,
foto_url = ? foto_url = ?
WHERE id_transporte = ? WHERE id_transporte = ?
"; ";
$params = [$vehiculo, $identFiscal, $idTrans, $fotoUrl, $id]; $params = [$vehiculo, $identFiscal, $idTrans, $fotoUrl, $id];
} else { } else {
@@ -190,7 +259,15 @@ function actualizar() {
$stmt = sqlsrv_query($conn, $sql, $params); $stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) { 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'); header('Location: /IMPORTADORES/transportes/lista?updated=ok');

View File

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

View File

@@ -44,7 +44,7 @@
<option value="">-- Selecciona un transportista --</option> <option value="">-- Selecciona un transportista --</option>
<?php foreach ($transportistas as $t): ?> <?php foreach ($transportistas as $t): ?>
<option value="<?= $t['id_transportista'] ?>"> <option value="<?= $t['id_transportista'] ?>">
<?= htmlspecialchars($t['nombre']) ?> <?= htmlspecialchars($t['clave_identificador'] . ' - ' . $t['nombre'] . ' - ' . $t['ciudad_nombre'] . ' - ' . $t['domicilio']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -65,6 +65,11 @@
<input name="numero_licencia" id="numero_licencia" type="text" maxlength="11" class="form-control" required> <input name="numero_licencia" id="numero_licencia" type="text" maxlength="11" class="form-control" required>
</div> </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"> <div class="mb-3">
<label for="telefono" class="form-label">Teléfono</label> <label for="telefono" class="form-label">Teléfono</label>
<input name="telefono" id="telefono" type="tel" maxlength="11" class="form-control"> <input name="telefono" id="telefono" type="tel" maxlength="11" class="form-control">
@@ -101,6 +106,7 @@
const nombre = document.getElementById('nombre').value.trim(); const nombre = document.getElementById('nombre').value.trim();
const apellido = document.getElementById('apellido').value.trim(); const apellido = document.getElementById('apellido').value.trim();
const numero_licencia = document.getElementById('numero_licencia').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 telefono = document.getElementById('telefono').value.trim();
const email = document.getElementById('email').value.trim(); const email = document.getElementById('email').value.trim();
const fecha_ingreso = document.getElementById('fecha_ingreso').value; const fecha_ingreso = document.getElementById('fecha_ingreso').value;
@@ -132,6 +138,11 @@
document.getElementById('numero_licencia').focus(); document.getElementById('numero_licencia').focus();
return; 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)) { 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' 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; return;
} }
} }
// VALIDACIÓN ASÍNCRONA DE DUPLICADO DE GAFETE
// SI TODAS LAS VALIDACIONES PASAN, ENVIAR EL FORMULARIO fetch(`/IMPORTADORES/choferes/validarNumeroGafete?numero_gafete=${encodeURIComponent(numero_gafete)}`)
this.submit(); .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> </script>

View File

@@ -46,7 +46,7 @@
<?php foreach ($transportistas as $t): ?> <?php foreach ($transportistas as $t): ?>
<option value="<?= $t['id_transportista'] ?>" <option value="<?= $t['id_transportista'] ?>"
<?= $chofer['transportista_id'] == $t['id_transportista'] ? 'selected' : '' ?>> <?= $chofer['transportista_id'] == $t['id_transportista'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['nombre']) ?> <?= htmlspecialchars($t['clave_identificador'] . ' - ' . $t['nombre'] . ' - ' . $t['ciudad_nombre'] . ' - ' . $t['domicilio']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -76,6 +76,13 @@
value="<?= htmlspecialchars($chofer['numero_licencia']) ?>" required> value="<?= htmlspecialchars($chofer['numero_licencia']) ?>" required>
</div> </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 --> <!-- Teléfono -->
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="telefono" class="form-label">Teléfono</label> <label for="telefono" class="form-label">Teléfono</label>
@@ -138,6 +145,7 @@
const nombre = document.getElementById('nombre').value.trim(); const nombre = document.getElementById('nombre').value.trim();
const apellido = document.getElementById('apellido').value.trim(); const apellido = document.getElementById('apellido').value.trim();
const numero_licencia = document.getElementById('numero_licencia').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 telefono = document.getElementById('telefono').value.trim();
const email = document.getElementById('email').value.trim(); const email = document.getElementById('email').value.trim();
const fecha_ingreso = document.getElementById('fecha_ingreso').value; const fecha_ingreso = document.getElementById('fecha_ingreso').value;
@@ -169,6 +177,11 @@
document.getElementById('numero_licencia').focus(); document.getElementById('numero_licencia').focus();
return; 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)) { 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' }); 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(); document.getElementById('Clave').focus();
@@ -211,9 +224,24 @@
return; 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 fetch(`/IMPORTADORES/choferes/validarNumeroGafete?numero_gafete=${encodeURIComponent(numero_gafete)}&id_chofer={id_chofer}`)
this.submit(); .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> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,7 +135,7 @@
</select> </select>
</div> </div>
<div class="col-md-4 mb-3"> <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/*"> <input id="foto_solicitud" name="foto_solicitud" type="file" class="form-control" accept="image/*">
</div> </div>
</div> </div>
@@ -217,135 +217,134 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
<script> <script>
// 1) Inicializar Choices para todos los selects excepto proveedor_id document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.searchable:not(#proveedor_id)').forEach(el => { // Primero cargamos proveedores, luego inicializamos Choices en todos los selects
new Choices(el, { searchEnabled: true, itemSelectText: '', shouldSort: false }); cargarProveedores().then(() => {
inicializarChoicesGlobal();
});
// Inicializamos los eventos adicionales
inicializarEventos();
}); });
// 2) Cargar proveedores dinámicamente // Función para cargar proveedores (retorna promesa)
const proveedorEl = document.getElementById('proveedor_id'); function cargarProveedores() {
let proveedorChoices = null; 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') // Inicializamos Choices globalmente (todos los .searchable)
.then(res => res.ok ? res.json() : Promise.reject(res.status)) function inicializarChoicesGlobal() {
.then(json => { document.querySelectorAll('.searchable').forEach(el => {
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>'; if (el._choices) el._choices.destroy(); // destruye instancia previa si existe
json.results.forEach(item => { el._choices = new Choices(el, {
const opt = document.createElement('option'); searchEnabled: true,
opt.value = item.id; itemSelectText: '',
opt.text = item.text; shouldSort: false,
proveedorEl.add(opt); searchFields: ['label'] // 🔐 Solo busca en el texto visible
}); });
// destruir instancia previa si existe });
if (proveedorChoices) proveedorChoices.destroy(); }
proveedorChoices = new Choices(proveedorEl, {
searchEnabled: true, // Eventos principales del formulario
itemSelectText: '', function inicializarEventos() {
shouldSort: false // 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) { // Eliminar partidas
proveedorChoices.setChoiceByValue(current); 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 // Control overflow tabla cuando se abre el dropdown
document.getElementById('add-partida').addEventListener('click', () => { document.addEventListener('click', function(e) {
const tbody = document.querySelector('#tabla-partidas tbody'); const tableContainer = document.querySelector('.table-responsive');
const idx = tbody.querySelectorAll('tr').length; if (!tableContainer) return;
const row = document.createElement('tr'); if (e.target.closest('.choices__inner')) {
row.innerHTML = ` tableContainer.style.overflow = 'visible';
<td><input name="partidas[\${idx}][descripcion]" class="form-control"></td> } else {
<td><input name="partidas[\${idx}][cantidad_comercial]" type="number" step="0.0001" class="form-control"></td> tableContainer.style.overflow = 'auto';
<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 });
}); });
});
// Manejador para controlar el overflow al abrir dropdowns // Validación suma de partidas al enviar
document.addEventListener('click', function(e) { $('#solicitudForm').submit(function(e){
const tableContainer = document.querySelector('.table-responsive'); const total = parseFloat($('#valor_factura').val()) || 0;
if (!tableContainer) return; let sum = 0;
$('.valor-partida').each(function(){ sum += parseFloat($(this).val()) || 0; });
if (e.target.closest('.choices__inner')) { if(Math.abs(sum - total) > 0.001){
tableContainer.style.overflow = 'visible'; e.preventDefault();
} else { Swal.fire({
tableContainer.style.overflow = 'auto'; icon:'error',
} title:'Error de validación',
}); text:`La suma de partidas (${sum.toFixed(2)}) no coincide con Valor Factura (${total.toFixed(2)}).`
});
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>';
}); });
}
</script> </script>
</body> </body>

View File

@@ -145,7 +145,7 @@
</select> </select>
</div> </div>
<div class="col-md-4 mb-3"> <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/*"> <input id="foto_solicitud" name="foto_solicitud" type="file" class="form-control" accept="image/*">
</div> </div>
</div> </div>
@@ -243,7 +243,6 @@
</div> </div>
</div> </div>
<!-- Choices.js & jQuery -->
<!-- Choices.js JS --> <!-- Choices.js JS -->
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script> <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> <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; } .sidebar .nav-link.active { background-color: #e9ecef; color: #212529; }
} }
.card { border-radius: 12px; } .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> </style>
</head> </head>
<body> <body>
@@ -39,12 +46,20 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Contenedor *</label> <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>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Identificador fiscal *</label> <label class="form-label">Identificación fiscal *</label>
<input name="identificador_fiscal" id="identFiscal" class="form-control" required> <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>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Foto (opcional)</label> <label class="form-label">Foto (opcional)</label>
<input type="file" name="foto" id="foto" class="form-control" accept="image/*"> <input type="file" name="foto" id="foto" class="form-control" accept="image/*">
@@ -55,7 +70,7 @@
<option value="">Selecciona...</option> <option value="">Selecciona...</option>
<?php foreach($transportistas as $tr): ?> <?php foreach($transportistas as $tr): ?>
<option value="<?= $tr['id_transportista'] ?>"> <option value="<?= $tr['id_transportista'] ?>">
<?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'])) ?> <?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'] . ' - ' . $tr['ciudad_nombre'] . ' - ' . $tr['domicilio'])) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -68,38 +83,70 @@
</form> </form>
</div> </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> <script>
document.getElementById('formTransCrear').addEventListener('submit', function(e) { document.getElementById('formTransCrear').addEventListener('submit', function(e) {
e.preventDefault(); // Prevenir envío por defecto
// Campos que validamos: // Campos que validamos:
const veh = document.getElementById('vehiculo').value.trim(); const vehiculo = document.getElementById('vehiculo').value.trim();
const fisc = document.getElementById('identFiscal').value.trim(); const ident_fiscal = document.getElementById('ident_fiscal').value.trim();
const trans = document.getElementById('transportista').value; const transportista = document.getElementById('transportista').value;
const fotoF = document.getElementById('foto').files[0]; const fotoF = document.getElementById('foto').files[0];
// Validaciones de campos obligatorios // Validaciones de campos obligatorios
if (!veh) { if (!vehiculo) {
e.preventDefault(); Swal.fire({ icon:'error', title:'Contenedor requerido', text:'Por favor ingresa el número económico del vehículo.', confirmButtonColor: '#dc3545'});
Swal.fire({ icon:'error', title:'Vehículo requerido', text:'Por favor ingresa el nombre del vehículo.', confirmButtonColor: '#dc3545'}); document.getElementById('vehiculo').focus();
document.querySelector('input[name="veh"]').focus();
return; return;
} }
if (!fisc) { if (!ident_fiscal) {
e.preventDefault(); Swal.fire({ icon:'error', title:'Identificación fiscal requerida', text:'Por favor ingresa la identificación fiscal.', confirmButtonColor: '#dc3545' });
Swal.fire({ icon:'error', title:'Identificador fiscal requerido', text:'Por favor ingresa el identificador fiscal.', confirmButtonColor: '#dc3545' }); document.getElementById('ident_fiscal').focus();
document.querySelector('input[name="fisc"]').focus();
return; return;
} }
if (!trans) { if (!transportista) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Transportista no seleccionado', text:'Debes elegir un transportista.', confirmButtonColor: '#dc3545' }); 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; return;
} }
if (fotoF && fotoF.size > 2 * 1024 * 1024) { // 2 MB 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.' }); 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. // Si todas las validaciones pasan, el formulario se envía.
this.submit();
}); });
</script> </script>

View File

@@ -41,13 +41,19 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Contenedor *</label> <label class="form-label">Contenedor *</label>
<input name="vehiculo" id="vehEdit" class="form-control" <div class="d-flex align-items-center">
value="<?= htmlspecialchars($t['vehiculo']) ?>" required> <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>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Identificador fiscal *</label> <label class="form-label">Identificación fiscal *</label>
<input name="identificador_fiscal" id="fiscEdit" class="form-control" <div class="d-flex align-items-center">
value="<?= htmlspecialchars($t['identificador_fiscal']) ?>" required> <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>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Foto actual</label><br> <label class="form-label">Foto actual</label><br>
@@ -59,15 +65,15 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Reemplazar foto (opcional)</label> <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>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Transportista *</label> <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): ?> <?php foreach($transportistas as $tr): ?>
<option value="<?= $tr['id_transportista'] ?>" <option value="<?= $tr['id_transportista'] ?>"
<?= $tr['id_transportista']==$t['id_transportista']?'selected':'' ?>> <?= $tr['id_transportista']==$t['id_transportista']?'selected':'' ?>>
<?= htmlspecialchars($tr['nombre']) ?> <?= htmlspecialchars(($tr['clave_identificador'] . ' - ' . $tr['nombre'] . ' - ' . $tr['ciudad_nombre'] . ' - ' . $tr['domicilio'])) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -80,38 +86,70 @@
</form> </form>
</div> </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> <script>
document.getElementById('formTransEdit').addEventListener('submit', function(e) { document.getElementById('formTransEdit').addEventListener('submit', function(e) {
e.preventDefault(); // Prevenir envío por defecto
// Campos que validamos: // Campos que validamos:
const veh = document.getElementById('vehEdit').value.trim(); const veh = document.getElementById('vehiculo').value.trim();
const fisc = document.getElementById('fiscEdit').value.trim(); const fisc = document.getElementById('identificador_fiscal').value.trim();
const trans = document.getElementById('trEdit').value; const trans = document.getElementById('transportista').value;
const fotoF = document.getElementById('fotoEdit').files[0]; const fotoF = document.getElementById('foto').files[0];
// Validaciones de campos obligatorios // Validaciones de campos obligatorios
if (!veh) { if (!veh) {
e.preventDefault(); Swal.fire({ icon:'error', title:'Contenedor requerido', text:'Por favor ingresa el número económico del vehículo.', confirmButtonColor: '#dc3545'});
Swal.fire({ icon:'error', title:'Vehículo requerido', text:'Por favor ingresa el nombre del vehículo.', confirmButtonColor: '#dc3545'}); document.getElementById('vehiculo').focus();
document.querySelector('input[name="veh"]').focus();
return; return;
} }
if (!fisc) { if (!fisc) {
e.preventDefault(); Swal.fire({ icon:'error', title:'Identificación fiscal requerida', text:'Por favor ingresa la identificación fiscal.', confirmButtonColor: '#dc3545' });
Swal.fire({ icon:'error', title:'Identificador fiscal requerido', text:'Por favor ingresa el identificador fiscal.', confirmButtonColor: '#dc3545' }); document.getElementById('identificador_fiscal').focus();
document.querySelector('input[name="fisc"]').focus();
return; return;
} }
if (!trans) { if (!trans) {
e.preventDefault();
Swal.fire({ icon:'error', title:'Transportista no seleccionado', text:'Debes elegir un transportista.', confirmButtonColor: '#dc3545' }); 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; return;
} }
if (fotoF && fotoF.size > 2 * 1024 * 1024) { // 2 MB 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.' }); 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. // Si todas las validaciones pasan, el formulario se envía.
this.submit();
}); });
</script> </script>

View File

@@ -57,7 +57,7 @@
<label class="form-label">Archivo CSV *</label> <label class="form-label">Archivo CSV *</label>
<input type="file" name="csv" accept=".csv" class="form-control" required> <input type="file" name="csv" accept=".csv" class="form-control" required>
</div> </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"> <a href="/IMPORTADORES/public/downloads/transportes_masivo_template.csv" class="btn btn-outline-secondary mb-3">
📥 Descargar plantilla 📥 Descargar plantilla
</a><br> </a><br>

View File

@@ -48,7 +48,7 @@
<div class="col-md-4"> <div class="col-md-4">
<label for="curp" class="form-label">CURP *</label> <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>
<div class="col-md-4"> <div class="col-md-4">
@@ -57,7 +57,7 @@
</div> </div>
<div class="col-md-4"> <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> <input name="caat" id="caat" class="form-control" maxlength="20" required>
</div> </div>
@@ -89,7 +89,7 @@
<div class="col-md-8"> <div class="col-md-8">
<label for="domicilio" class="form-label">Domicilio *</label> <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>
</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 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 rfcRegex = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
const telefonoRegex = /^\d{3}\s\d{7}$/; const telefonoRegex = /^\d{3}\s\d{7}$/;
const soloNumerosRegex = /^[0-9]+$/;
// Validaciones de campos obligatorios // Validaciones de campos obligatorios
if (!clave) { if (!clave) {
@@ -151,11 +150,6 @@
document.getElementById('rfc').focus(); document.getElementById('rfc').focus();
return; 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ó) // Validación de CURP (solo si se ingresó)
if (curp && !curpRegex.test(curp)) { if (curp && !curpRegex.test(curp)) {
Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545' Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545'
@@ -179,12 +173,7 @@
return; return;
} }
if (!caat) { if (!caat) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código CAAT es obligatorio.', confirmButtonColor: '#dc3545' }); 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' });
document.getElementById('caat').focus(); document.getElementById('caat').focus();
return; return;
} }

View File

@@ -63,7 +63,7 @@
id="telefono" class="form-control" maxlength="11" required> id="telefono" class="form-control" maxlength="11" required>
</div> </div>
<div class="col-md-4"> <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']) ?>" <input name="caat" value="<?= htmlspecialchars($t['caat']) ?>"
id="caat" class="form-control" maxlength="20" required> id="caat" class="form-control" maxlength="20" required>
</div> </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 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 rfcRegex = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
const telefonoRegex = /^\d{3}\s\d{7}$/; const telefonoRegex = /^\d{3}\s\d{7}$/;
const soloNumerosRegex = /^[0-9]+$/;
// Validaciones de campos obligatorios // Validaciones de campos obligatorios
if (!clave) { if (!clave) {
@@ -168,11 +167,6 @@
document.getElementById('rfc').focus(); document.getElementById('rfc').focus();
return; 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ó) // Validación de CURP (solo si se ingresó)
if (curp && !curpRegex.test(curp)) { if (curp && !curpRegex.test(curp)) {
Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545' Swal.fire({ icon: 'error', title: 'CURP inválido', text: 'El CURP no tiene el formato correcto.', confirmButtonColor: '#dc3545'
@@ -196,12 +190,7 @@
return; return;
} }
if (!caat) { if (!caat) {
Swal.fire({ icon: 'error', title: 'Campo requerido', text: 'El código CAAT es obligatorio.', confirmButtonColor: '#dc3545' }); 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' });
document.getElementById('caat').focus(); document.getElementById('caat').focus();
return; return;
} }

View File

@@ -67,29 +67,30 @@
$('#transportistas-table').DataTable({ $('#transportistas-table').DataTable({
serverSide: true, serverSide: true,
processing: 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: { ajax: {
url: '/IMPORTADORES/transportistas/ajax_lista', url: '/IMPORTADORES/transportistas/ajax_lista',
type: 'GET' type: 'GET'
}, },
columns: [ columns: [
{ data: 0 }, { data: 0 }, // ID
{ data: 1 }, { data: 1 }, // Código
{ data: 2 }, { data: 2 }, // Nombre
{ data: 3 }, { data: 3 }, // RFC
{ data: 4 }, { data: 4 }, // Ciudad
{ data: 5 }, { data: 5 }, // Fecha
{ {
data: null, data: null,
orderable: false, orderable: false,
searchable: false, searchable: false,
render: function(row) { render: function(data, type, row) {
const id = row[0]; const id = row[0];
return ` return `
<a href="/IMPORTADORES/transportistas/editar?id=${id}" class="btn btn-sm btn-primary">✏️</a> <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> <button class="btn btn-sm btn-danger" onclick="confirmDelete(${id})">🗑️</button>
`; `;
} }
} }
], ],
language: { language: {