Productos frecuenes

This commit is contained in:
2025-07-16 12:43:21 -06:00
parent 8a3e174894
commit cf86fc21cd
10 changed files with 981 additions and 624 deletions

View File

@@ -5,8 +5,61 @@ require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../vendor/autoload.php';
// 2) Carga nuestro helper de entorno y dispara la carga de .env
require_once __DIR__ . '/../helpers/env.php';
loadEnv();
/** Obtiene (y cachea en sesión) el JWT de la API usando las credenciales de $_ENV **/
/** Obtiene (y cachea) el JWT de la API usando credenciales de $_ENV
* Gestiona expiración: asume 1 hora de vida y renueva si ha pasado. **/
function getApiToken(): ?string
{
// Duración en segundos del token (60 min)
$ttl = 3600;
// 1) Si ya tenemos token y no ha expirado, lo devolvemos
if (!empty($_SESSION['api_token']) && !empty($_SESSION['api_token_time'])) {
$age = time() - $_SESSION['api_token_time'];
if ($age < $ttl) {
error_log("[getApiToken] Usando token en caché (edad: {$age}s)");
return $_SESSION['api_token'];
}
error_log("[getApiToken] Token expirado (edad: {$age}s), obteniendo uno nuevo");
}
// 2) Sí o sí hacemos login en la API
$url = rtrim($_ENV['API_URL'] ?? '', '/') . '/auth/login';
$user = $_ENV['API_USER'] ?? '';
$pass = $_ENV['API_PASS'] ?? '';
$body = json_encode(['username' => $user, 'password' => $pass]);
error_log("[getApiToken] POST $url");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("[getApiToken] HTTP $http");
if ($http === 200 && ($data = json_decode($resp, true)) && !empty($data['token'])) {
// 3) Guardamos token y tiempo actual
$_SESSION['api_token'] = $data['token'];
$_SESSION['api_token_time'] = time();
return $data['token'];
}
// 4) Si no se pudo obtener, devolvemos null
error_log('[getApiToken] No se pudo obtener token de la API');
return null;
}
/** GET /IMPORTADORES/productos_frecuentes
* Muestra el listado **/
function index()
@@ -14,6 +67,90 @@ function index()
lista();
}
function lista()
{
if (empty($_SESSION['usuario_id'])) {
header('Location: /IMPORTADORES/login');
exit;
}
$conn = getConnection();
// === Obtener proveedores desde API ===
$token = getApiToken();
$proveedoresApi = [];
if ($token) {
$apiBase = rtrim($_ENV['API_URL'] ?? '', '/');
$url = $apiBase . '/proveedores';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 200 && ($json = json_decode($resp, true)) && is_array($json)) {
foreach ($json as $p) {
$clave = $p['Clave'] ?? '';
$nombre = $p['Nombre'] ?? '';
if ($clave) {
$proveedoresApi[$clave] = $nombre;
}
}
}
}
// === Consulta de productos frecuentes ===
$sql = "SELECT
pf.id_producto_frecuente,
pf.sinonimo,
pf.fraccion,
pf.nico,
pf.numero_parte,
pf.proveedor AS proveedor_guardado,
u.descripcion AS unidad_de_medida,
por.nombre AS pais_origen_destino,
pc.nombre AS pais_comprador_vendedor,
pf.uso_mercancia,
pf.estado_mercancia,
pf.preferencia,
pf.frecuencia_uso
FROM dbo.productos_frecuentes pf
LEFT JOIN dbo.unidades_medida_apendice7 u
ON pf.umc_id = u.id
LEFT JOIN dbo.paises por
ON pf.pais_origen_destino = por.nombre
LEFT JOIN dbo.paises pc
ON pf.pais_comprador_vendedor = pc.nombre
WHERE pf.id_importador = ?
AND pf.status = 1
ORDER BY pf.fecha_alta DESC
";
$stmt = sqlsrv_query($conn, $sql, [$_SESSION['usuario_id']]);
if ($stmt === false) {
echo '<pre>', print_r(sqlsrv_errors(), true), '</pre>';
exit;
}
// === Construir arreglo de productos con nombre de proveedor ===
$productos = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$clave = $row['proveedor_guardado'];
$row['nombre_proveedor'] = $proveedoresApi[$clave] ?? 'N/D';
$productos[] = $row;
}
// === Cargar la vista con productos y nombres de proveedor ===
include __DIR__ . '/../../views/productos_frecuentes/lista.php';
}
function ajax_paises()
{
// Sólo importadores pueden usarlo
@@ -58,7 +195,7 @@ function ajax_proveedores()
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Accept: application/json'],
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
@@ -84,14 +221,30 @@ function ajax_proveedores()
exit;
}
function lista()
function ajax_unidades()
{
if (empty($_SESSION['usuario_id'])) {
header('Location: /IMPORTADORES/login');
// Sólo importadores pueden usarlo
if (empty($_SESSION['usuario_id']) || $_SESSION['tipo_usuario'] !== 'importador') {
http_response_code(401);
echo json_encode(['results' => []]);
exit;
}
include __DIR__ . '/../../views/productos_frecuentes/lista.php';
$conn = getConnection();
$sql = "SELECT id, descripcion FROM dbo.unidades_medida_apendice7 ORDER BY descripcion";
$stmt = sqlsrv_query($conn, $sql);
$out = ['results' => []];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$out['results'][] = [
'id' => $row['id'],
'text' => $row['descripcion']
];
}
echo json_encode($out);
exit;
}
/** GET /IMPORTADORES/productos_frecuentes/alta
@@ -117,38 +270,118 @@ function guardar()
$conn = getConnection();
$errores = [];
$id_importador = $_SESSION['usuario_id'];
// Recoger y sanear
$sinonimo = trim($_POST['sinonimo'] ?? '');
$fraccion = trim($_POST['fraccion'] ?? '');
$nico = trim($_POST['nico'] ?? '');
$numero_parte = trim($_POST['numero_parte'] ?? null);
$descripcion = trim($_POST['descripcion'] ?? null);
$numero_parte = trim($_POST['numero_parte'] ?? '');
$descripcion = trim($_POST['descripcion'] ?? '');
$umc_id = intval($_POST['umc_id'] ?? 0);
$pais_origen_destino = trim($_POST['pais_origen_destino'] ?? null);
$pais_comprador_vendedor = trim($_POST['pais_comprador_vendedor'] ?? null);
$uso_mercancia = trim($_POST['uso_mercancia'] ?? null);
$estado_mercancia = trim($_POST['estado_mercancia'] ?? null);
$vinculacion = trim($_POST['vinculacion'] ?? null);
$observaciones = trim($_POST['observaciones'] ?? null);
$preferencia = trim($_POST['preferencia'] ?? null);
$criterio_preferencia = trim($_POST['criterio_preferencia'] ?? null);
$uso_producto = trim($_POST['uso_producto'] ?? null);
$descripcion_producto = trim($_POST['descripcion_producto'] ?? null);
$certificado_origen = isset($_POST['certificado_origen']) ? 1 : 0;
$tipo_mercancia = trim($_POST['tipo_mercancia'] ?? null);
$pais_origen_destino = trim($_POST['pais_origen_destino'] ?? '');
$pais_comprador_vendedor = trim($_POST['pais_comprador_vendedor'] ?? '');
$uso_mercancia = trim($_POST['uso_mercancia'] ?? '');
$estado_mercancia = trim($_POST['estado_mercancia'] ?? '');
$vinculacion = trim($_POST['vinculacion'] ?? '');
$observaciones = trim($_POST['observaciones'] ?? '');
$preferencia = trim($_POST['preferencia'] ?? '');
$criterio_preferencia = trim($_POST['criterio_preferencia'] ?? '');
$uso_producto = trim($_POST['uso_producto'] ?? '');
$descripcion_producto = trim($_POST['descripcion_producto'] ?? '');
$certificado_origen = trim($_POST['certificado_origen'] ?? '');
$tipo_mercancia = trim($_POST['tipo_mercancia'] ?? '');
$documento_en_original = isset($_POST['documento_en_original']) ? 1 : 0;
$proveedor = trim($_POST['proveedor'] ?? null);
$id_importador = intval($_SESSION['usuario_id']);
$status = 1;
$proveedor = trim($_POST['proveedor'] ?? '');
$status = intval($_POST['status'] ?? 1);
$frecuencia_uso = intval($_POST['frecuencia_uso'] ?? 1);
// Validación mínima
if ($sinonimo === '' || $fraccion === '' || $nico === '' || $umc_id <= 0) {
$_SESSION['flash_error'] = 'Sinónimo, Fracción, NICO y Unidad de Medida son obligatorios.';
// Validación de campos obligatorios (según tu formulario)
if ($sinonimo === '') {
$errores[] = 'El sinónimo es obligatorio';
}
if ($fraccion === '') {
$errores[] = 'La fracción es obligatoria';
}
if ($nico === '') {
$errores[] = 'El NICO es obligatorio';
}
if ($numero_parte === '') {
$errores[] = 'El número de parte es obligatorio';
}
if ($pais_origen_destino === '') {
$errores[] = 'El país de origen/destino es obligatorio';
}
if ($pais_comprador_vendedor === '') {
$errores[] = 'El país comprador/vendedor es obligatorio';
}
if ($umc_id <= 0) {
$errores[] = 'La unidad de medida es obligatoria';
}
// Validar que NICO solo contenga números
if (!preg_match('/^[0-9]+$/', $nico)) {
$errores[] = 'El NICO solo puede contener números';
}
// **SOLUCIÓN PRINCIPAL: Convertir IDs de países a nombres**
$pais_origen_nombre = null;
$pais_comprador_nombre = null;
if (!empty($pais_origen_destino)) {
$sql_pais = "SELECT nombre FROM dbo.paises WHERE id_pais = ?";
$stmt_pais = sqlsrv_query($conn, $sql_pais, [intval($pais_origen_destino)]);
if ($stmt_pais && $row = sqlsrv_fetch_array($stmt_pais, SQLSRV_FETCH_ASSOC)) {
$pais_origen_nombre = $row['nombre'];
} else {
$errores[] = 'País de origen/destino no válido';
}
}
if (!empty($pais_comprador_vendedor)) {
$sql_pais = "SELECT nombre FROM dbo.paises WHERE id_pais = ?";
$stmt_pais = sqlsrv_query($conn, $sql_pais, [intval($pais_comprador_vendedor)]);
if ($stmt_pais && $row = sqlsrv_fetch_array($stmt_pais, SQLSRV_FETCH_ASSOC)) {
$pais_comprador_nombre = $row['nombre'];
} else {
$errores[] = 'País comprador/vendedor no válido';
}
}
// Validar que existe la unidad de medida
$sql_umc = "SELECT COUNT(*) as count FROM dbo.unidades_medida_apendice7 WHERE id = ?";
$stmt_umc = sqlsrv_query($conn, $sql_umc, [$umc_id]);
if ($stmt_umc && $row = sqlsrv_fetch_array($stmt_umc, SQLSRV_FETCH_ASSOC)) {
if ($row['count'] == 0) {
$errores[] = 'Unidad de medida no válida';
}
}
// Si hay errores, regresar
if (!empty($errores)) {
$_SESSION['flash_error'] = implode('. ', $errores);
header('Location: /IMPORTADORES/productos_frecuentes/alta');
exit;
}
// Convertir valores vacíos a NULL para campos opcionales
if ($numero_parte === '') $numero_parte = null;
if ($descripcion === '') $descripcion = null;
if ($uso_mercancia === '') $uso_mercancia = null;
if ($estado_mercancia === '') $estado_mercancia = null;
if ($vinculacion === '') $vinculacion = null;
if ($observaciones === '') $observaciones = null;
if ($preferencia === '') $preferencia = null;
if ($criterio_preferencia === '') $criterio_preferencia = null;
if ($uso_producto === '') $uso_producto = null;
if ($descripcion_producto === '') $descripcion_producto = null;
if ($tipo_mercancia === '') $tipo_mercancia = null;
if ($proveedor === '') $proveedor = null;
// INSERT
$sql = "INSERT INTO dbo.productos_frecuentes
(sinonimo, fraccion, nico, numero_parte, descripcion, umc_id,
@@ -160,20 +393,26 @@ function guardar()
";
$params = [
$sinonimo, $fraccion, $nico, $numero_parte, $descripcion, $umc_id,
$pais_origen_destino, $pais_comprador_vendedor, $uso_mercancia, $estado_mercancia, $vinculacion,
$pais_origen_nombre, $pais_comprador_nombre, $uso_mercancia, $estado_mercancia, $vinculacion,
$observaciones, $preferencia, $criterio_preferencia, $uso_producto, $descripcion_producto,
$certificado_origen, $tipo_mercancia, $documento_en_original, $proveedor,
$id_importador, $status, $frecuencia_uso
];
// Para debug - remover en producción
error_log("Datos a insertar: " . print_r($params, true));
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
$_SESSION['flash_error'] = 'Error al guardar el producto frecuente.';
$errors = sqlsrv_errors();
error_log("Error en SQL: " . print_r($errors, true));
$_SESSION['flash_error'] = 'Error al guardar el producto.';
header('Location: /IMPORTADORES/productos_frecuentes/alta');
exit;
}
$_SESSION['flash_success'] = 'Producto frecuente registrado correctamente.';
$_SESSION['flash_success'] = 'Producto registrado correctamente.';
header('Location: /IMPORTADORES/productos_frecuentes');
exit;
}
@@ -205,6 +444,17 @@ function editar()
exit;
}
// Recuperar IDs reales desde nombres de país
foreach (['pais_origen_destino', 'pais_comprador_vendedor'] as $campo) {
$nombre = $producto[$campo];
$stmtPais = sqlsrv_query($conn, "SELECT id_pais FROM dbo.paises WHERE nombre = ?", [$nombre]);
if ($stmtPais && ($row = sqlsrv_fetch_array($stmtPais, SQLSRV_FETCH_ASSOC))) {
$producto[$campo] = $row['id_pais'];
} else {
$producto[$campo] = null;
}
}
include __DIR__ . '/../../views/productos_frecuentes/editar.php';
}
@@ -219,29 +469,91 @@ function actualizar()
$conn = getConnection();
$id = intval($_POST['id_producto_frecuente'] ?? 0);
$sinonimo = trim($_POST['sinonimo'] ?? '');
$errores = [];
$id = intval($_POST['id_producto_frecuente'] ?? 0);
$sinonimo = trim($_POST['sinonimo'] ?? '');
$fraccion = trim($_POST['fraccion'] ?? '');
$nico = trim($_POST['nico'] ?? '');
$numero_parte = trim($_POST['numero_parte'] ?? '');
$descripcion = trim($_POST['descripcion'] ?? '');
$umc_id = intval($_POST['umc_id'] ?? 0);
$pais_origen_destino = trim($_POST['pais_origen_destino'] ?? '');
$pais_comprador_vendedor = trim($_POST['pais_comprador_vendedor'] ?? '');
$uso_mercancia = trim($_POST['uso_mercancia'] ?? '');
$estado_mercancia = trim($_POST['estado_mercancia'] ?? '');
$vinculacion = trim($_POST['vinculacion'] ?? '');
$observaciones = trim($_POST['observaciones'] ?? '');
$preferencia = trim($_POST['preferencia'] ?? '');
$criterio_preferencia = trim($_POST['criterio_preferencia'] ?? '');
$uso_producto = trim($_POST['uso_producto'] ?? '');
$descripcion_producto = trim($_POST['descripcion_producto'] ?? '');
$certificado_origen = trim($_POST['certificado_origen'] ?? '');
$tipo_mercancia = trim($_POST['tipo_mercancia'] ?? '');
$documento_en_original = isset($_POST['documento_en_original']) ? 1 : 0;
$proveedor = trim($_POST['proveedor'] ?? '');
// Validar países
$pais_origen_nombre = null;
$pais_comprador_nombre = null;
if (!empty($pais_origen_destino)) {
$sql_pais = "SELECT nombre FROM dbo.paises WHERE id_pais = ?";
$stmt_pais = sqlsrv_query($conn, $sql_pais, [intval($pais_origen_destino)]);
if ($stmt_pais && ($row = sqlsrv_fetch_array($stmt_pais, SQLSRV_FETCH_ASSOC))) {
$pais_origen_nombre = $row['nombre'];
} else {
$errores[] = 'País de origen/destino no válido';
}
}
if (!empty($pais_comprador_vendedor)) {
$sql_pais = "SELECT nombre FROM dbo.paises WHERE id_pais = ?";
$stmt_pais = sqlsrv_query($conn, $sql_pais, [intval($pais_comprador_vendedor)]);
if ($stmt_pais && ($row = sqlsrv_fetch_array($stmt_pais, SQLSRV_FETCH_ASSOC))) {
$pais_comprador_nombre = $row['nombre'];
} else {
$errores[] = 'País comprador/vendedor no válido';
}
}
// Validar unidad
$sql_umc = "SELECT COUNT(*) as count FROM dbo.unidades_medida_apendice7 WHERE id = ?";
$stmt_umc = sqlsrv_query($conn, $sql_umc, [$umc_id]);
if (!$stmt_umc || ($row = sqlsrv_fetch_array($stmt_umc, SQLSRV_FETCH_ASSOC))['count'] == 0) {
$errores[] = 'Unidad de medida no válida';
}
// Si hay errores
if (!empty($errores)) {
$_SESSION['flash_error'] = implode('. ', $errores);
header("Location: /IMPORTADORES/productos_frecuentes/editar?id=$id");
exit;
}
// UPDATE
$sql = "UPDATE dbo.productos_frecuentes SET
sinonimo = ?, fraccion = ?, nico = ?, numero_parte = ?, descripcion = ?, umc_id = ?,
pais_origen_destino = ?, pais_comprador_vendedor = ?, uso_mercancia = ?, estado_mercancia = ?, vinculacion = ?,
observaciones = ?, preferencia = ?, criterio_preferencia = ?, uso_producto = ?, descripcion_producto = ?,
certificado_origen = ?, tipo_mercancia = ?, documento_en_original = ?, proveedor = ?
sinonimo = ?, fraccion = ?, nico = ?, numero_parte = ?, descripcion = ?,
umc_id = ?, pais_origen_destino = ?, pais_comprador_vendedor = ?,
uso_mercancia = ?, estado_mercancia = ?, vinculacion = ?, observaciones = ?,
preferencia = ?, criterio_preferencia = ?, uso_producto = ?, descripcion_producto = ?,
certificado_origen = ?, tipo_mercancia = ?, documento_en_original = ?, proveedor = ?
WHERE id_producto_frecuente = ?
";
$params = [
$sinonimo, $fraccion, $nico, $numero_parte, $descripcion, $umc_id,
$pais_origen_destino, $pais_comprador_vendedor, $uso_mercancia, $estado_mercancia, $vinculacion,
$observaciones, $preferencia, $criterio_preferencia, $uso_producto, $descripcion_producto,
$certificado_origen, $tipo_mercancia, $documento_en_original, $proveedor, $id
$sinonimo, $fraccion, $nico, $numero_parte, $descripcion, $umc_id,
$pais_origen_nombre, $pais_comprador_nombre, $uso_mercancia, $estado_mercancia, $vinculacion,
$observaciones, $preferencia, $criterio_preferencia, $uso_producto, $descripcion_producto,
$certificado_origen, $tipo_mercancia, $documento_en_original, $proveedor, $id
];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
$_SESSION['flash_error'] = 'Error al actualizar el producto frecuente.';
$_SESSION['flash_error'] = 'Error al actualizar el producto.';
} else {
$_SESSION['flash_success'] = 'Producto frecuente actualizado correctamente.';
$_SESSION['flash_success'] = 'Producto actualizado correctamente.';
}
header('Location: /IMPORTADORES/productos_frecuentes');
exit;
}
@@ -258,32 +570,6 @@ function importacion_csv()
include __DIR__ . '/../../views/productos_frecuentes/importacion_csv.php';
}
function ajax_unidades()
{
// Sólo importadores pueden usarlo
if (empty($_SESSION['usuario_id']) || $_SESSION['tipo_usuario'] !== 'importador') {
http_response_code(401);
echo json_encode(['results' => []]);
exit;
}
$conn = getConnection();
$sql = "SELECT id, descripcion FROM dbo.unidades_medida_apendice7 ORDER BY descripcion";
$stmt = sqlsrv_query($conn, $sql);
$out = ['results' => []];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$out['results'][] = [
'id' => $row['id'],
'text' => $row['descripcion']
];
}
echo json_encode($out);
exit;
}
/** POST /IMPORTADORES/productos_frecuentes/procesar_csv
* Procesa el upload y la inserción de CSV **/
function procesar_csv()
@@ -321,3 +607,33 @@ function procesar_csv()
header('Location: /IMPORTADORES/productos_frecuentes');
exit;
}
function eliminar()
{
if (empty($_SESSION['usuario_id'])) {
header('Location: /IMPORTADORES/login');
exit;
}
$id = $_GET['id'] ?? null;
if (!$id || !is_numeric($id)) {
http_response_code(400);
echo "ID inválido";
return;
}
$conn = getConnection();
$sql = "UPDATE productos_frecuentes SET status = 0 WHERE id_producto_frecuente = ? AND id_importador = ?";
$params = [$id, $_SESSION['usuario_id']];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
echo '<pre>', print_r(sqlsrv_errors(), true), '</pre>';
exit;
}
header('Location: /IMPORTADORES/productos_frecuentes?deleted=ok');
exit;
}

View File

@@ -931,7 +931,7 @@ function generarPedimentoDesdeSolicitud($conn, $id, $usuarioId, $token)
$ch = curl_init($urlPed);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Content-Type: application/json'],
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $jsonPayload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
@@ -1025,7 +1025,7 @@ function ajax_lista()
// 4) Ejecutamos cURL
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Accept: application/json'],
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
@@ -1332,7 +1332,7 @@ function update_status()
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
"Authorization: $token",
"Authorization: Bearer $token",
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => $jsonPayload,
@@ -1448,15 +1448,15 @@ function enviarNotificacionCambioStatus($email, $nombreCompleto, $datosSolicitud
/** Obtiene información del status (descripción y emoji) **/
function obtenerInfoStatus($status) {
$statusMap = [
1 => ['emoji' => '🔄', 'descripcion' => 'En proceso', 'color' => '#17a2b8'],
1 => ['emoji' => '🔄', 'descripcion' => 'En proceso', 'color' => '#17a2b8'],
2 => ['emoji' => '📋', 'descripcion' => 'Solicitar importación', 'color' => '#ffc107'],
3 => ['emoji' => '🏢', 'descripcion' => 'Con agencia aduana', 'color' => '#6f42c1'],
4 => ['emoji' => '💳', 'descripcion' => 'En proceso de pago', 'color' => '#fd7e14'],
5 => ['emoji' => '✅', 'descripcion' => 'Pedimento generado', 'color' => '#28a745'],
6 => ['emoji' => '📦', 'descripcion' => 'En tránsito', 'color' => '#007bff'],
7 => ['emoji' => '🏁', 'descripcion' => 'Entregado', 'color' => '#28a745'],
8 => ['emoji' => '❌', 'descripcion' => 'Cancelado', 'color' => '#dc3545'],
9 => ['emoji' => '⏸️', 'descripcion' => 'Suspendido', 'color' => '#6c757d']
3 => ['emoji' => '🏢', 'descripcion' => 'Con agencia aduanal', 'color' => '#6f42c1'],
4 => ['emoji' => '💳', 'descripcion' => 'En proceso de pago', 'color' => '#fd7e14'],
5 => ['emoji' => '✅', 'descripcion' => 'Pedimento generado', 'color' => '#28a745'],
6 => ['emoji' => '📦', 'descripcion' => 'En tránsito', 'color' => '#007bff'],
7 => ['emoji' => '🏁', 'descripcion' => 'Entregado', 'color' => '#28a745'],
8 => ['emoji' => '❌', 'descripcion' => 'Cancelado', 'color' => '#dc3545'],
9 => ['emoji' => '⏸️', 'descripcion' => 'Suspendido', 'color' => '#6c757d']
];
return $statusMap[$status] ?? [
@@ -1703,7 +1703,7 @@ function obtenerProveedorPorClave($clave) {
// Ejecutar cURL para obtener todos los proveedores
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Accept: application/json'],
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);

11
productos_frecuentes.txt Normal file
View File

@@ -0,0 +1,11 @@
-- 1. Elimina la restricción DEFAULT
ALTER TABLE [Importaciones_HC].[dbo].[productos_frecuentes]
DROP CONSTRAINT [DF__productos__certi__3EDC53F0];
-- 2. Modifica el tipo de dato
ALTER TABLE [Importaciones_HC].[dbo].[productos_frecuentes]
ALTER COLUMN [certificado_origen] NVARCHAR(100);
-- 3. (Opcional) Si quieres volver a agregar un valor por defecto
ALTER TABLE [Importaciones_HC].[dbo].[productos_frecuentes]
ADD CONSTRAINT DF_productos_certificado_origen DEFAULT '' FOR [certificado_origen];

View File

@@ -319,8 +319,7 @@
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'
});
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('numero_licencia').focus();
return;
}

View File

@@ -168,12 +168,14 @@
</a>
<a href="/IMPORTADORES/productos_frecuentes/editar"
class="nav-link px-3 py-1 <?= str_contains($_SERVER['REQUEST_URI'], '/productos_frecuentes/editar') ? 'active' : '' ?>">
Editar Producto
Ver Productos
</a>
<!--
<a href="/IMPORTADORES/productos_frecuentes/importacion_csv"
class="nav-link px-3 py-1 <?= str_contains($_SERVER['REQUEST_URI'], '/productos_frecuentes/importacion_csv') ? 'active' : '' ?>">
• Importación Masiva CSV
</a>
-->
</nav>
</div>
@@ -364,12 +366,14 @@
</a>
<a href="/IMPORTADORES/productos_frecuentes/editar"
class="nav-link px-3 py-1 <?= str_contains($_SERVER['REQUEST_URI'], '/productos_frecuentes/editar') ? 'active' : '' ?>">
Editar Producto
Ver Productos
</a>
<!--
<a href="/IMPORTADORES/productos_frecuentes/importacion_csv"
class="nav-link px-3 py-1 <?= str_contains($_SERVER['REQUEST_URI'], '/productos_frecuentes/importacion_csv') ? 'active' : '' ?>">
• Importación Masiva CSV
</a>
-->
</nav>
</div>

View File

@@ -1,23 +1,12 @@
<!-- views/productos_frecuentes/alta.php -->
<?php
include __DIR__ . '/../partials/sidebar_importador.php';
// Opciones de Vinculación y Criterio Preferencia
$vinculaciones = [
'0' => 'No existe',
'1' => 'Existe, no afecta',
'2' => 'Existe y afecta'
];
$criterios = [
'A' => 'Criterio A',
'B' => 'Criterio B',
'C' => 'Criterio C',
'D' => 'Criterio D',
'E' => 'Criterio E',
'F' => 'Criterio F'
];
$vinculaciones = [ '0' => 'No existe', '1' => 'Existe, no afecta', '2' => 'Existe y afecta' ];
$criterios = [ 'A' => 'Criterio A', 'B' => 'Criterio B', 'C' => 'Criterio C', 'D' => 'Criterio D', 'E' => 'Criterio E', 'F' => 'Criterio F' ];
// Proveedores, unidades y países se cargan por AJAX
?>
<!DOCTYPE html>
<html lang="es">
<head>
@@ -48,9 +37,8 @@
.card-hover:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.15) !important; }
.btn-pulse { animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.fade-in-up { animation: fadeInUp 0.8s ease-out; }
@keyframes fadeInUp {
@@ -137,27 +125,28 @@
<form id="altaProductoFrecuenteForm" action="/IMPORTADORES/productos_frecuentes/guardar" method="POST">
<div class="row g-3 form-group-animated">
<!-- Sinónimo, Fracción, NICO -->
<div class="col-md-4 form-group-animated">
<div class="col-md-6 form-group-animated">
<label for="sinonimo" class="form-label">Sinónimo *</label>
<input type="text" id="sinonimo" name="sinonimo" class="form-control" required>
</div>
<div class="col-md-4 form-group-animated">
<label for="fraccion" class="form-label">Fracción *</label>
<input type="text" id="fraccion" name="fraccion" class="form-control" required>
<input type="text" id="fraccion" name="fraccion" class="form-control" maxlegth="8" required>
</div>
<div class="col-md-4 form-group-animated">
<div class="col-md-2 form-group-animated">
<label for="nico" class="form-label">NICO *</label>
<input type="text" id="nico" name="nico" class="form-control" required>
<input type="text" id="nico" name="nico" class="form-control" maxlength="2" required>
</div>
<!-- Número de Parte / Proveedor -->
<div class="col-md-6 form-group-animated">
<label for="numero_parte" class="form-label">Número de Parte</label>
<input type="text" id="numero_parte" name="numero_parte" class="form-control">
<div class="col-md-5 form-group-animated">
<label for="numero_parte" class="form-label">Número de Parte *</label>
<input type="text" id="numero_parte" name="numero_parte" class="form-control" required>
</div>
<div class="col-md-6">
<label for="proveedor" class="form-label">Proveedor *</label>
<select id="proveedor" name="proveedor" class="form-select searchable" required>
<div class="col-md-3"></div>
<div class="col-md-4">
<label for="proveedor" class="form-label">Proveedor</label>
<select id="proveedor" name="proveedor" class="form-select searchable">
<option value="">Cargando proveedores…</option>
</select>
</div>
@@ -169,35 +158,42 @@
</div>
<!-- País Origen/Destino -->
<div class="col-md-4">
<label for="pais_origen_destino" class="form-label">País Origen/Destino</label>
<select id="pais_origen_destino" name="pais_origen_destino" class="form-select searchable" required>
<div class="col-md-3">
<label for="pais_origen_destino" class="form-label">País Origen/Destino *</label>
<select id="pais_origen_destino" name="pais_origen_destino" class="form-select searchable">
<option value="">Cargando países…</option>
</select>
</div>
<!-- País Comprador/Vendedor -->
<div class="col-md-4">
<label for="pais_comprador_vendedor" class="form-label">País Comprador/Vendedor</label>
<select id="pais_comprador_vendedor" name="pais_comprador_vendedor" class="form-select searchable" required>
<div class="col-md-3">
<label for="pais_comprador_vendedor" class="form-label">País Comprador/Vendedor *</label>
<select id="pais_comprador_vendedor" name="pais_comprador_vendedor" class="form-select searchable">
<option value="">Cargando países…</option>
</select>
</div>
<!-- Uso de la Mercancía -->
<div class="col-md-4 form-group-animated">
<div class="col-md-3 form-group-animated">
<label for="uso_mercancia" class="form-label">Uso de la Mercancía</label>
<input type="text" id="uso_mercancia" name="uso_mercancia" class="form-control">
</div>
<!-- Estado de la Mercancia -->
<div class="col-md-3 form-group-animated">
<label for="estado_mercancia" class="form-label">Estado de la Mercancía</label>
<input type="text" id="estado_mercancia" name="estado_mercancia" class="form-control">
</div>
<!-- Unidad de Medida -->
<div class="col-md-4">
<div class="col-md-3">
<label for="umc_id" class="form-label">Unidad de Medida *</label>
<select id="umc_id" name="umc_id" class="form-select searchable" required>
<select id="umc_id" name="umc_id" class="form-select searchable">
<option value="">Cargando unidades…</option>
</select>
</div>
<!-- Vinculación -->
<div class="col-md-4">
<div class="col-md-3">
<label for="vinculacion" class="form-label">Vinculación</label>
<select id="vinculacion" name="vinculacion" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -208,7 +204,7 @@
</div>
<!-- Preferencia (fijas) -->
<div class="col-md-4">
<div class="col-md-3">
<label for="preferencia" class="form-label">Preferencia</label>
<select id="preferencia" name="preferencia" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -220,8 +216,8 @@
</select>
</div>
<!-- Criterio Preferencia / Tipo Mercancía -->
<div class="col-md-6">
<!-- Criterio Preferencia -->
<div class="col-md-3">
<label for="criterio_preferencia" class="form-label">Criterio Preferencia</label>
<!-- Agrega `disabled` al select de criterio -->
<select id="criterio_preferencia" name="criterio_preferencia" class="form-select searchable" disabled>
@@ -231,18 +227,27 @@
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 form-group-animated">
<!-- Tipo Mercancía -->
<div class="col-md-5 form-group-animated">
<label for="tipo_mercancia" class="form-label">Tipo de Mercancía</label>
<input type="text" id="tipo_mercancia" name="tipo_mercancia" class="form-control">
</div>
<div class="col-md-2"></div>
<!-- Checkboxes -->
<div class="col-md-3 form-group-animated">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="certificado_origen" name="certificado_origen" value="1">
<label class="form-check-label" for="certificado_origen">Certificado de Origen</label>
</div>
<!-- Certificado de Origen -->
<div class="col-md-5 form-group-animated">
<label for="certificado_origen" class="form-label">Certificado de Origen</label>
<input type="text" id="certificado_origen" name="certificado_origen" class="form-control">
</div>
<!-- Observaciones -->
<div class="col-12 form-group-animated">
<label for="observaciones" class="form-label">Observaciones</label>
<textarea id="observaciones" name="observaciones" class="form-control" rows="2"></textarea>
</div>
<!-- Checkbox -->
<div class="col-md-3 form-group-animated">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="documento_en_original" name="documento_en_original" value="1">
@@ -251,9 +256,9 @@
</div>
<!-- Ocultos -->
<input type="hidden" name="id_importador" value="<?= $_SESSION['usuario_id'] ?>">
<input type="hidden" name="status" value="1">
<input type="hidden" name="frecuencia_uso" value="1">
<input type="hidden" name="id_importador" value="<?= $_SESSION['usuario_id'] ?>">
<input type="hidden" name="status" value="1">
<input type="hidden" name="frecuencia_uso" value="1">
<!-- Botones -->
<div class="col-12 text-end mt-4 form-group-animated">
@@ -272,6 +277,88 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
<script>
document.getElementById('altaProductoFrecuenteForm').addEventListener('submit', function(e) {
e.preventDefault();
const sinonimo = document.getElementById('sinonimo').value.trim();
const fraccion = document.getElementById('fraccion').value.trim();
const nico = document.getElementById('nico').value.trim();
const numero_parte = document.getElementById('numero_parte').value.trim();
const pais_o_d = document.getElementById('pais_origen_destino').value;
const pais_c_v = document.getElementById('pais_comprador_vendedor').value;
const unidad_medida = document.getElementById('umc_id').value;
const soloNumRegex = /^[0-9]+$/;
if (!sinonimo) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el sinónimo.', confirmButtonColor: '#dc3545' });
return document.getElementById('sinonimo').focus();
}
if (!fraccion) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa la fracción.', confirmButtonColor: '#dc3545' });
return document.getElementById('fraccion').focus();
}
if (!soloNumRegex.test(fraccion)) {
Swal.fire({ icon: 'error', title: 'Fracción inválida', text: 'La fracción solo puede contener números', confirmButtonColor: '#dc3545' });
document.getElementById('fraccion').focus();
return;
}
if (!nico) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el NICO.', confirmButtonColor: '#dc3545' });
return document.getElementById('nico').focus();
}
if (!soloNumRegex.test(nico)) {
Swal.fire({ icon: 'error', title: 'NICO inválido', text: 'El NICO solo puede contener números', confirmButtonColor: '#dc3545' });
document.getElementById('nico').focus();
return;
}
if (!numero_parte) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el número de parte.', confirmButtonColor: '#dc3545' });
return document.getElementById('numero_parte').focus();
}
// Función helper para hacer focus en elementos con Choices.js
function focusChoicesElement(selectId) {
const choicesContainer = document.querySelector(`#${selectId}`).parentElement.querySelector('.choices');
if (choicesContainer) {
choicesContainer.focus();
}
}
// En tu validación del submit:
if (!pais_o_d) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona el país de origen.', confirmButtonColor: '#dc3545' });
focusChoicesElement('pais_origen_destino');
return;
}
if (!pais_c_v) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona el país de destino.', confirmButtonColor: '#dc3545' });
focusChoicesElement('pais_comprador_vendedor');
return;
}
if (!unidad_medida) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona la unidad de medida.', confirmButtonColor: '#dc3545' });
focusChoicesElement('umc_id');
return;
}
// Obtener todos los inputs y selects para validación
const inputs = document.querySelectorAll('#altaProductoFrecuenteForm input[required], #altaProductoFrecuenteForm select[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
input.classList.add('shake');
isValid = false;
}
});
if (isValid) {
// Si pasa la validación, enviamos el formulario manualmente
this.submit();
}
});
document.addEventListener('DOMContentLoaded', () => {
function fetchJsonOrThrow(url) {
return fetch(url)
@@ -368,7 +455,7 @@
// Script para manejar las animaciones de validación
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input.form-control, select.form_select');
const inputs = document.querySelectorAll('input.form-control, select.form_select');
const selects = document.querySelectorAll('select.form_select');
inputs.forEach(input => {
@@ -412,19 +499,6 @@
}
}
}
// Validación del formulario
document.getElementById('altaProductoFrecuenteForm').addEventListener('submit', function(e) {
e.preventDefault();
let isValid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
input.classList.add('shake');
isValid = false;
}
});
});
});
</script>

View File

@@ -1,22 +1,13 @@
<!-- views/productos_frecuentes/editar.php -->
<?php
include __DIR__ . '/../partials/sidebar_importador.php';
$vinculaciones = [
'0' => 'No existe',
'1' => 'Existe, no afecta',
'2' => 'Existe y afecta'
];
$criterios = [
'A' => 'Criterio A',
'B' => 'Criterio B',
'C' => 'Criterio C',
'D' => 'Criterio D',
'E' => 'Criterio E',
'F' => 'Criterio F'
];
// Opciones de Vinculación y Criterio Preferencia
$vinculaciones = [ '0' => 'No existe', '1' => 'Existe, no afecta', '2' => 'Existe y afecta' ];
$criterios = [ 'A' => 'Criterio A', 'B' => 'Criterio B', 'C' => 'Criterio C', 'D' => 'Criterio D', 'E' => 'Criterio E', 'F' => 'Criterio F' ];
// Proveedores, unidades y países se cargan por AJAX
// $producto proviene del controlador
?>
<!DOCTYPE html>
<html lang="es">
<head>
@@ -41,9 +32,8 @@
.card-hover:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.15) !important; }
.btn-pulse { animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.fade-in-up { animation: fadeInUp 0.8s ease-out; }
@keyframes fadeInUp {
@@ -63,6 +53,51 @@
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s; }
.btn-animated:hover::before { left: 100%; }
.form-label { font-weight: 500; }
/* ========== ANIMACIONES PARA INPUTS TEXTO ========== */
.form-control:not(.no-animation) { transition: all 0.3s ease; border: 2px solid #dee2e6; position: relative; }
.form-control:not(.no-animation):focus { border-color: #0d6efd; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25), 0 0 20px rgba(13, 110, 253, 0.3); transform: translateY(-2px); }
.form-control:not(.no-animation):not(:placeholder-shown) { /*border-color: #198754; background-color: #f8fff9;*/ }
.form-control:not(.no-animation).shake { animation: shake 0.5s ease-in-out; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* Floating labels para inputs texto */
.form-floating-custom { position: relative; margin-bottom: 1.5rem; }
.form-floating-custom .form-control { padding: 1rem 0.75rem 0.5rem 0.75rem; height: auto; }
.form-floating-custom .form-label { position: absolute; top: 0.75rem; left: 0.75rem; transition: all 0.3s ease; pointer-events: none; color: #6c757d; z-index: 2; }
.form-floating-custom .form-control:focus ~ .form-label,
.form-floating-custom .form-control:not(:placeholder-shown) ~ .form-label { top: 0.25rem; font-size: 0.75rem; color: #0d6efd; font-weight: 600; }
.input-pulse:hover:not(.no-animation) { animation: inputPulse 0.6s ease-in-out; }
@keyframes inputPulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
.input-gradient:not(.no-animation) { position: relative; overflow: hidden; }
.input-gradient:not(.no-animation)::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(13, 110, 253, 0.3), transparent); transition: left 0.5s; pointer-events: none; z-index: 1; }
.input-gradient:not(.no-animation):focus::before { left: 100%; }
.form-control.valid:not(.no-animation) { /*border-color: #198754; background: linear-gradient(45deg, #fff, #f8fff9);*/ animation: successGlow 1s ease-in-out; }
@keyframes successGlow {
0% { box-shadow: 0 0 5px rgba(25, 135, 84, 0.5); }
50% { box-shadow: 0 0 20px rgba(25, 135, 84, 0.8); }
100% { box-shadow: 0 0 5px rgba(25, 135, 84, 0.5); }
}
/* Animación para los campos del formulario */
.form-group-animated { opacity: 0; transform: translateY(20px); animation: slideUp 0.6s ease-out forwards; }
.form-group-animated:nth-child(1) { animation-delay: 0.1s; }
.form-group-animated:nth-child(2) { animation-delay: 0.2s; }
.form-group-animated:nth-child(3) { animation-delay: 0.3s; }
.form-group-animated:nth-child(4) { animation-delay: 0.4s; }
.form-group-animated:nth-child(5) { animation-delay: 0.5s; }
.form-group-animated:nth-child(6) { animation-delay: 0.6s; }
.form-group-animated:nth-child(7) { animation-delay: 0.7s; }
.form-group-animated:nth-child(8) { animation-delay: 0.8s; }
.form-group-animated:nth-child(9) { animation-delay: 0.9s; }
.form-group-animated:nth-child(10) { animation-delay: 1.0s; }
@keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
@@ -82,65 +117,74 @@
<div class="card p-4 shadow-sm bg-white card-hover position-relative h-auto">
<form id="editarProductoFrecuenteForm" action="/IMPORTADORES/productos_frecuentes/actualizar" method="POST">
<input type="hidden" name="id_producto_frecuente" value="<?= htmlspecialchars($producto['id_producto_frecuente']) ?>">
<div class="row g-3">
<div class="col-md-4">
<label for="sinonimo" class="form-label">Sinónimo *</label>
<div class="row g-3 form-group-animated">
<div class="col-md-6 form-group-animated">
<label for="sinonimo" class="form-label">Sinónimo</label>
<input type="text" id="sinonimo" name="sinonimo" class="form-control"
value="<?= htmlspecialchars($producto['sinonimo']) ?>" required>
value="<?= htmlspecialchars($producto['sinonimo']) ?>">
</div>
<div class="col-md-4">
<label for="fraccion" class="form-label">Fracción *</label>
<input type="text" id="fraccion" name="fraccion" class="form-control"
value="<?= htmlspecialchars($producto['fraccion']) ?>" required>
<div class="col-md-4 form-group-animated">
<label for="fraccion" class="form-label">Fracción</label>
<input type="text" id="fraccion" name="fraccion" class="form-control" maxlegth="8"
value="<?= htmlspecialchars($producto['fraccion']) ?>">
</div>
<div class="col-md-4">
<label for="nico" class="form-label">NICO *</label>
<input type="text" id="nico" name="nico" class="form-control"
value="<?= htmlspecialchars($producto['nico']) ?>" required>
<div class="col-md-2 form-group-animated">
<label for="nico" class="form-label">NICO</label>
<input type="text" id="nico" name="nico" class="form-control" maxlength="2"
value="<?= htmlspecialchars($producto['nico']) ?>">
</div>
<div class="col-md-6">
<div class="col-md-5 form-group-animated">
<label for="numero_parte" class="form-label">Número de Parte</label>
<input type="text" id="numero_parte" name="numero_parte" class="form-control"
value="<?= htmlspecialchars($producto['numero_parte']) ?>">
</div>
<div class="col-md-6">
<label for="proveedor" class="form-label">Proveedor *</label>
<select id="proveedor" name="proveedor" class="form-select searchable" required>
<div class="col-md-3"></div>
<div class="col-md-4">
<label for="proveedor" class="form-label">Proveedor</label>
<select id="proveedor" name="proveedor" class="form-select searchable">
<option value="">Cargando proveedores…</option>
</select>
</div>
<div class="col-12">
<div class="col-12 form-group-animated">
<label for="descripcion" class="form-label">Descripción</label>
<textarea id="descripcion" name="descripcion" class="form-control" rows="2"><?= htmlspecialchars($producto['descripcion']) ?></textarea>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label for="pais_origen_destino" class="form-label">País Origen/Destino</label>
<select id="pais_origen_destino" name="pais_origen_destino" class="form-select searchable" required>
<select id="pais_origen_destino" name="pais_origen_destino" class="form-select searchable">
<option value="">Cargando países…</option>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label for="pais_comprador_vendedor" class="form-label">País Comprador/Vendedor</label>
<select id="pais_comprador_vendedor" name="pais_comprador_vendedor" class="form-select searchable" required>
<select id="pais_comprador_vendedor" name="pais_comprador_vendedor" class="form-select searchable">
<option value="">Cargando países…</option>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3 form-group-animated">
<label for="uso_mercancia" class="form-label">Uso de la Mercancía</label>
<input type="text" id="uso_mercancia" name="uso_mercancia" class="form-control"
value="<?= htmlspecialchars($producto['uso_mercancia']) ?>">
</div>
<div class="col-md-4">
<label for="umc_id" class="form-label">Unidad de Medida *</label>
<select id="umc_id" name="umc_id" class="form-select searchable" required>
<div class="col-md-3 form-group-animated">
<label for="estado_mercancia" class="form-label">Estado de la Mercancía</label>
<input type="text" id="estado_mercancia" name="estado_mercancia" class="form-control"
value="<?= htmlspecialchars($producto['estado_mercancia']) ?>">
</div>
<div class="col-md-3">
<label for="umc_id" class="form-label">Unidad de Medida</label>
<select id="umc_id" name="umc_id" class="form-select searchable">
<option value="">Cargando unidades…</option>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label for="vinculacion" class="form-label">Vinculación</label>
<select id="vinculacion" name="vinculacion" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -151,7 +195,8 @@
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label for="preferencia" class="form-label">Preferencia</label>
<select id="preferencia" name="preferencia" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -161,7 +206,7 @@
</select>
</div>
<div class="col-md-6">
<div class="col-md-3">
<label for="criterio_preferencia" class="form-label">Criterio Preferencia</label>
<select id="criterio_preferencia" name="criterio_preferencia" class="form-select searchable" disabled>
<option value="">-- Selecciona --</option>
@@ -172,20 +217,26 @@
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<div class="col-md-5 form-group-animated">
<label for="tipo_mercancia" class="form-label">Tipo de Mercancía</label>
<input type="text" id="tipo_mercancia" name="tipo_mercancia" class="form-control"
value="<?= htmlspecialchars($producto['tipo_mercancia']) ?>">
</div>
<div class="col-md-2"></div>
<div class="col-md-3">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="certificado_origen" name="certificado_origen" value="1"
<?= $producto['certificado_origen']?'checked':'' ?>>
<label class="form-check-label" for="certificado_origen">Certificado de Origen</label>
</div>
<div class="col-md-5 form-group-animated">
<label for="certificado_origen" class="form-label">Certificado de Origen</label>
<input type="text" id="certificado_origen" name="certificado_origen" class="form-control"
value="<?= htmlspecialchars($producto['certificado_origen']) ?>">
</div>
<div class="col-md-3">
<div class="col-12 form-group-animated">
<label for="observaciones" class="form-label">Observaciones</label>
<textarea id="observaciones" name="observaciones" class="form-control" rows="2"><?= htmlspecialchars($producto['observaciones']) ?></textarea>
</div>
<div class="col-md-3 form-group-animated">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="documento_en_original" name="documento_en_original" value="1"
<?= $producto['documento_en_original']?'checked':'' ?>>
@@ -194,7 +245,7 @@
</div>
</div>
<div class="mt-4 text-end">
<div class="col-12 mt-4 text-end form-group-animated">
<button type="submit" class="btn btn-primary mt-auto w-auto btn-animated">Actualizar</button>
<a href="/IMPORTADORES/productos_frecuentes" class="btn btn-secondary ms-2 mt-auto w-auto btn-animated">Cancelar</a>
</div>
@@ -206,118 +257,200 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Helper para fetch + validación JSON
function fetchJsonOrThrow(url) {
return fetch(url)
.then(resp => {
if (!resp.ok) throw new Error(`HTTP ${resp.status} en ${url}`);
return resp.text();
})
.then(txt => {
try {
return JSON.parse(txt);
} catch (e) {
console.error(`Respuesta no-JSON en ${url}:`, txt);
throw e;
document.getElementById('editarProductoFrecuenteForm').addEventListener('submit', function(e) {
e.preventDefault();
const sinonimo = document.getElementById('sinonimo').value.trim();
const fraccion = document.getElementById('fraccion').value.trim();
const nico = document.getElementById('nico').value.trim();
const numero_parte = document.getElementById('numero_parte').value.trim();
const pais_o_d = document.getElementById('pais_origen_destino').value;
const pais_c_v = document.getElementById('pais_comprador_vendedor').value;
const unidad_medida = document.getElementById('umc_id').value;
const soloNumRegex = /^[0-9]+$/;
if (!sinonimo) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el sinónimo.', confirmButtonColor: '#dc3545' });
return document.getElementById('sinonimo').focus();
}
if (!fraccion) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa la fracción.', confirmButtonColor: '#dc3545' });
return document.getElementById('fraccion').focus();
}
if (!soloNumRegex.test(fraccion)) {
Swal.fire({ icon: 'error', title: 'Fracción inválida', text: 'La fracción solo puede contener números', confirmButtonColor: '#dc3545' });
document.getElementById('fraccion').focus();
return;
}
if (!nico) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el NICO.', confirmButtonColor: '#dc3545' });
return document.getElementById('nico').focus();
}
if (!soloNumRegex.test(nico)) {
Swal.fire({ icon: 'error', title: 'NICO inválido', text: 'El NICO solo puede contener números', confirmButtonColor: '#dc3545' });
document.getElementById('nico').focus();
return;
}
if (!numero_parte) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor ingresa el número de parte.', confirmButtonColor: '#dc3545' });
return document.getElementById('numero_parte').focus();
}
// Función helper para hacer focus en elementos con Choices.js
function focusChoicesElement(selectId) {
const choicesContainer = document.querySelector(`#${selectId}`).parentElement.querySelector('.choices');
if (choicesContainer) {
choicesContainer.focus();
}
}
// En tu validación del submit:
if (!pais_o_d) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona el país de origen.', confirmButtonColor: '#dc3545' });
focusChoicesElement('pais_origen_destino');
return;
}
if (!pais_c_v) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona el país de destino.', confirmButtonColor: '#dc3545' });
focusChoicesElement('pais_comprador_vendedor');
return;
}
if (!unidad_medida) {
Swal.fire({ icon: 'warning', title: 'Campo requerido', text: 'Por favor selecciona la unidad de medida.', confirmButtonColor: '#dc3545' });
focusChoicesElement('umc_id');
return;
}
// Obtener todos los inputs y selects para validación
const inputs = document.querySelectorAll('#altaProductoFrecuenteForm input[required], #altaProductoFrecuenteForm select[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
input.classList.add('shake');
isValid = false;
}
});
}
// Referencias a los <select>
const selProv = document.getElementById('proveedor');
const selUmc = document.getElementById('umc_id');
const selPor = document.getElementById('pais_origen_destino');
const selPcv = document.getElementById('pais_comprador_vendedor');
const prefEl = document.getElementById('preferencia');
const critEl = document.getElementById('criterio_preferencia');
// Valores previamente guardados (inyectados por PHP)
const savedProv = <?= json_encode($producto['proveedor']) ?>;
const savedUmc = <?= json_encode($producto['umc_id']) ?>;
const savedPor = <?= json_encode($producto['pais_origen_destino']) ?>;
const savedPcv = <?= json_encode($producto['pais_comprador_vendedor']) ?>;
const savedPref = <?= json_encode($producto['preferencia']) ?>;
const savedCrit = <?= json_encode($producto['criterio_preferencia']) ?>;
Promise.all([
fetchJsonOrThrow('/IMPORTADORES/solicitud_importacion/ajax_proveedores'),
fetchJsonOrThrow('/IMPORTADORES/productos_frecuentes/ajax_unidades'),
fetchJsonOrThrow('/IMPORTADORES/productos_frecuentes/ajax_paises')
])
.then(([provData, umcData, paisData]) => {
// Proveedores
selProv.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
provData.results.forEach(it => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (it.id == savedProv) o.selected = true;
selProv.appendChild(o);
});
// Unidades de Medida
selUmc.innerHTML = '<option value="">-- Selecciona UMC --</option>';
umcData.results.forEach(it => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (it.id == savedUmc) o.selected = true;
selUmc.appendChild(o);
});
// Países de Origen/Destino y Comprador/Vendedor
selPor.innerHTML = '<option value="">-- Selecciona País --</option>';
selPcv.innerHTML = '<option value="">-- Selecciona País --</option>';
paisData.results.forEach(it => {
[selPor, selPcv].forEach(sel => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (sel === selPor && it.id == savedPor) o.selected = true;
if (sel === selPcv && it.id == savedPcv) o.selected = true;
sel.appendChild(o);
});
});
// Inicializar Choices.js en todos los .searchable
document.querySelectorAll('.searchable').forEach(el => {
if (el._choices) el._choices.destroy();
el._choices = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
});
// Restaurar y controlar Preferencia → Criterio
prefEl.value = savedPref || '';
function updateCriterio() {
if (prefEl.value === 'PROSEC') {
critEl.disabled = false;
critEl._choices.enable();
critEl.value = savedCrit || '';
critEl._choices.setChoiceByValue(savedCrit);
} else {
critEl._choices.removeActiveItems();
critEl._choices.disable();
critEl.disabled = true;
if (isValid) {
// Si pasa la validación, enviamos el formulario manualmente
this.submit();
}
}
// Inicial y evento
updateCriterio();
prefEl.addEventListener('change', updateCriterio);
})
.catch(err => {
console.error('Error en AJAX:', err);
Swal.fire({
icon: 'error',
title: 'Error cargando datos',
text: err.message
});
});
});
</script>
document.addEventListener('DOMContentLoaded', () => {
// Helper para fetch + validación JSON
function fetchJsonOrThrow(url) {
return fetch(url)
.then(resp => {
if (!resp.ok) throw new Error(`HTTP ${resp.status} en ${url}`);
return resp.text();
})
.then(txt => {
try {
return JSON.parse(txt);
} catch (e) {
console.error(`Respuesta no-JSON en ${url}:`, txt);
throw e;
}
});
}
// Referencias a los <select>
const selProv = document.getElementById('proveedor');
const selUmc = document.getElementById('umc_id');
const selPor = document.getElementById('pais_origen_destino');
const selPcv = document.getElementById('pais_comprador_vendedor');
const prefEl = document.getElementById('preferencia');
const critEl = document.getElementById('criterio_preferencia');
// Valores previamente guardados (inyectados por PHP)
const savedProv = <?= json_encode($producto['proveedor']) ?>;
const savedUmc = <?= json_encode($producto['umc_id']) ?>;
const savedPor = <?= json_encode($producto['pais_origen_destino']) ?>;
const savedPcv = <?= json_encode($producto['pais_comprador_vendedor']) ?>;
const savedPref = <?= json_encode($producto['preferencia']) ?>;
const savedCrit = <?= json_encode($producto['criterio_preferencia']) ?>;
Promise.all([
fetchJsonOrThrow('/IMPORTADORES/solicitud_importacion/ajax_proveedores'),
fetchJsonOrThrow('/IMPORTADORES/productos_frecuentes/ajax_unidades'),
fetchJsonOrThrow('/IMPORTADORES/productos_frecuentes/ajax_paises')
])
.then(([provData, umcData, paisData]) => {
// Proveedores
selProv.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
provData.results.forEach(it => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (it.id == savedProv) o.selected = true;
selProv.appendChild(o);
});
// Unidades de Medida
selUmc.innerHTML = '<option value="">-- Selecciona UMC --</option>';
umcData.results.forEach(it => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (it.id == savedUmc) o.selected = true;
selUmc.appendChild(o);
});
// Países de Origen/Destino y Comprador/Vendedor
selPor.innerHTML = '<option value="">-- Selecciona País --</option>';
selPcv.innerHTML = '<option value="">-- Selecciona País --</option>';
paisData.results.forEach(it => {
[selPor, selPcv].forEach(sel => {
const o = document.createElement('option');
o.value = it.id;
o.textContent = it.text;
if (sel === selPor && it.id == savedPor) o.selected = true;
if (sel === selPcv && it.id == savedPcv) o.selected = true;
sel.appendChild(o);
});
});
// Inicializar Choices.js en todos los .searchable
document.querySelectorAll('.searchable').forEach(el => {
if (el._choices) el._choices.destroy();
el._choices = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
});
// Restaurar y controlar Preferencia → Criterio
prefEl.value = savedPref || '';
function updateCriterio() {
if (prefEl.value === 'PROSEC') {
critEl.disabled = false;
critEl._choices.enable();
critEl.value = savedCrit || '';
critEl._choices.setChoiceByValue(savedCrit);
} else {
critEl._choices.removeActiveItems();
critEl._choices.disable();
critEl.disabled = true;
}
}
// Inicial y evento
updateCriterio();
prefEl.addEventListener('change', updateCriterio);
})
.catch(err => {
console.error('Error en AJAX:', err);
Swal.fire({
icon: 'error',
title: 'Error cargando datos',
text: err.message
});
});
});
</script>
</body>
</html>

View File

@@ -57,26 +57,19 @@
<?php if (!empty($_SESSION['flash_error'])): ?>
<script>
Swal.fire({
icon: 'error',
title: 'Error',
text: '<?= addslashes($_SESSION['flash_error']) ?>'
});
Swal.fire({ icon: 'error', title: 'Error', text: '<?= addslashes($_SESSION['flash_error']) ?>' });
</script>
<?php unset($_SESSION['flash_error']); endif; ?>
<?php if (!empty($_SESSION['flash_success'])): ?>
<script>
Swal.fire({
icon: 'success',
title: '¡Listo!',
text: '<?= addslashes($_SESSION['flash_success']) ?>'
});
Swal.fire({ icon: 'success', title: '¡Listo!', text: '<?= addslashes($_SESSION['flash_success']) ?>' });
</script>
<?php unset($_SESSION['flash_success']); endif; ?>
<div class="content">
<h4 class="mb-4 animate__animated animate__fadeInDown title_glow">📋 Productos Frecuentes</h4>
<a href="/IMPORTADORES/productos_frecuentes/alta" class="btn btn-success mb-3 mt-auto w-auto btn-animated"> Nuevo Producto</a>
<div class="card p-4 shadow-sm card-hover position-relative h-auto">
<div class="table-responsive">
<table id="tablaProductosFrecuentes" class="table table-striped table-bordered">
@@ -99,50 +92,14 @@
</tr>
</thead>
<tbody>
<?php
$conn = getConnection();
$sql = "
SELECT
pf.id_producto_frecuente,
pf.sinonimo,
pf.fraccion,
pf.nico,
pf.numero_parte,
pf.proveedor AS proveedor_guardado,
u.descripcion AS unidad_de_medida,
por.nombre AS pais_origen_destino,
pc.nombre AS pais_comprador_vendedor,
pf.uso_mercancia,
pf.estado_mercancia,
pf.preferencia,
pf.frecuencia_uso
FROM dbo.productos_frecuentes pf
LEFT JOIN dbo.unidades_medida_apendice7 u
ON pf.umc_id = u.id
LEFT JOIN dbo.paises por
ON pf.pais_origen_destino = por.id_pais
LEFT JOIN dbo.paises pc
ON pf.pais_comprador_vendedor = pc.id_pais
WHERE pf.id_importador = ?
ORDER BY pf.fecha_alta DESC
";
$stmt = sqlsrv_query($conn, $sql, [$_SESSION['usuario_id']]);
if ($stmt === false) {
echo '<pre>', print_r(sqlsrv_errors(), true), '</pre>';
exit;
}
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)):
?>
<?php foreach ($productos as $row): ?>
<tr>
<td><?= htmlspecialchars($row['id_producto_frecuente']) ?></td>
<td><?= htmlspecialchars($row['sinonimo']) ?></td>
<td><?= htmlspecialchars($row['fraccion']) ?></td>
<td><?= htmlspecialchars($row['nico']) ?></td>
<td><?= htmlspecialchars($row['numero_parte']) ?></td>
<td><?= htmlspecialchars($row['proveedor_guardado']) ?></td>
<td><?= htmlspecialchars($row['nombre_proveedor']) ?></td>
<td><?= htmlspecialchars($row['unidad_de_medida']) ?></td>
<td><?= htmlspecialchars($row['pais_origen_destino']) ?></td>
<td><?= htmlspecialchars($row['pais_comprador_vendedor']) ?></td>
@@ -151,16 +108,12 @@
<td><?= htmlspecialchars($row['preferencia']) ?></td>
<td><?= htmlspecialchars($row['frecuencia_uso']) ?></td>
<td class="text-center">
<a href="/IMPORTADORES/productos_frecuentes/editar?id=<?= $row['id_producto_frecuente'] ?>"
class="btn btn-sm btn-primary mt-auto w-auto btn-animated">✏️</a>
<button class="btn btn-sm btn-danger mt-auto w-auto btn-animated"
onclick="confirmDelete(<?= $row['id_producto_frecuente'] ?>)">
🗑️
</button>
<a href="/IMPORTADORES/productos_frecuentes/editar?id=<?= $row['id_producto_frecuente'] ?>" class="btn btn-sm btn-primary mt-auto w-auto btn-animated">✏️</a>
<button class="btn btn-sm btn-danger mt-auto w-auto btn-animated" onclick="confirmDelete(<?= $row['id_producto_frecuente'] ?>)">🗑️</button>
</td>
</tr>
<?php endwhile; ?>
</tbody>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
@@ -193,6 +146,11 @@
}
});
}
<?php if (isset($_GET['deleted']) && $_GET['deleted']==='ok'): ?>
Swal.fire({ icon: 'success', title: 'Producto eliminado', text: 'Ya no aparecerá en tu lista.', confirmButtonColor: '#198754' });
<?php endif; ?>
</script>
</body>
</html>

View File

@@ -8,13 +8,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Choices.js CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<!-- Sweet Alert 2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Animate.css para animaciones adicionales -->
@@ -59,80 +59,6 @@
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s; }
.btn-animated:hover::before { left: 100%; }
.hide { display: none !important; }
.btn-pulse { animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.fade-in-up { animation: fadeInUp 0.8s ease-out; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* Responsive animations */
@media (max-width: 768px) { .card-hover:hover { transform: translateY(-4px) scale(1.01); } }
/* Efecto de glow para elementos activos */
.btn.active { box-shadow: 0 0 20px rgba(var(--bs-primary-rgb), 0.5); }
/* Animación para el título */
.title-glow { text-shadow: 0 0 10px rgba(0, 123, 255, 0.3); }
/* Efecto para botones */
.btn-animated { transition: all 0.3s ease; position: relative; overflow: hidden; }
.btn-animated:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); }
.btn-animated::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s; }
.btn-animated:hover::before { left: 100%; }
.form-label { font-weight: 500; }
/* ========== ANIMACIONES PARA INPUTS TEXTO ========== */
.form-control:not(.no-animation) { transition: all 0.3s ease; border: 2px solid #dee2e6; position: relative; }
.form-control:not(.no-animation):focus { border-color: #0d6efd; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25), 0 0 20px rgba(13, 110, 253, 0.3); transform: translateY(-2px); }
.form-control:not(.no-animation):not(:placeholder-shown) { /*border-color: #198754; background-color: #f8fff9;*/ }
.form-control:not(.no-animation).shake { animation: shake 0.5s ease-in-out; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* Floating labels para inputs texto */
.form-floating-custom { position: relative; margin-bottom: 1.5rem; }
.form-floating-custom .form-control { padding: 1rem 0.75rem 0.5rem 0.75rem; height: auto; }
.form-floating-custom .form-label { position: absolute; top: 0.75rem; left: 0.75rem; transition: all 0.3s ease; pointer-events: none; color: #6c757d; z-index: 2; }
.form-floating-custom .form-control:focus ~ .form-label,
.form-floating-custom .form-control:not(:placeholder-shown) ~ .form-label { top: 0.25rem; font-size: 0.75rem; color: #0d6efd; font-weight: 600; }
.input-pulse:hover:not(.no-animation) { animation: inputPulse 0.6s ease-in-out; }
@keyframes inputPulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
.input-gradient:not(.no-animation) { position: relative; overflow: hidden; }
.input-gradient:not(.no-animation)::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(13, 110, 253, 0.3), transparent); transition: left 0.5s; pointer-events: none; z-index: 1; }
.input-gradient:not(.no-animation):focus::before { left: 100%; }
.form-control.valid:not(.no-animation) { /*border-color: #198754; background: linear-gradient(45deg, #fff, #f8fff9);*/ animation: successGlow 1s ease-in-out; }
@keyframes successGlow {
0% { box-shadow: 0 0 5px rgba(25, 135, 84, 0.5); }
50% { box-shadow: 0 0 20px rgba(25, 135, 84, 0.8); }
100% { box-shadow: 0 0 5px rgba(25, 135, 84, 0.5); }
}
/* Animación para los campos del formulario */
.form-group-animated { opacity: 0; transform: translateY(20px); animation: slideUp 0.6s ease-out forwards; }
.form-group-animated:nth-child(1) { animation-delay: 0.1s; }
.form-group-animated:nth-child(2) { animation-delay: 0.2s; }
.form-group-animated:nth-child(3) { animation-delay: 0.3s; }
.form-group-animated:nth-child(4) { animation-delay: 0.4s; }
.form-group-animated:nth-child(5) { animation-delay: 0.5s; }
.form-group-animated:nth-child(6) { animation-delay: 0.6s; }
.form-group-animated:nth-child(7) { animation-delay: 0.7s; }
.form-group-animated:nth-child(8) { animation-delay: 0.8s; }
.form-group-animated:nth-child(9) { animation-delay: 0.9s; }
.form-group-animated:nth-child(10) { animation-delay: 1.0s; }
@keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
/* ========== ESTILOS PARA CAMPO FOTO (SIN ANIMACIONES) ========== */
.no-animation, .no-border-style { transition: none !important; border: none; box-shadow: none !important; background: #fff !important; }
.no-animation:focus { transform: none !important; border: none; box-shadow: none !important; }
.no-animation:hover { animation: none !important; }
.no-animation::before, .no-animation::after { display: none !important; }
</style>
</head>
<body>
@@ -145,19 +71,19 @@
<!-- Datos principales -->
<div class="row">
<div class="col-md-3 mb-3 form-group-animated">
<div class="col-md-3 mb-3">
<label for="numero_factura" class="form-label">Número de Factura</label>
<input id="numero_factura" name="numero_factura" type="text" class="form-control" required
value="<?= htmlspecialchars($factura['numero_factura']) ?>">
</div>
<div class="col-md-3 mb-3 form-group-animated">
<div class="col-md-3 mb-3">
<label for="fecha_factura" class="form-label">Fecha de Factura</label>
<input id="fecha_factura" name="fecha_factura" type="date" class="form-control" required
value="<?= htmlspecialchars($factura['fecha_factura']) ?>">
</div>
<div class="col-md-3 mb-3">
<label for="proveedor_clave" class="form-label">Proveedor</label>
<select id="proveedor_clave" name="proveedor_clave" class="form-select searchable">
<label for="proveedor_id" class="form-label">Proveedor</label>
<select id="proveedor_id" name="proveedor_clave" class="form-select searchable">
<option value="">Cargando proveedores...</option>
</select>
</div>
@@ -224,7 +150,7 @@
<!-- Valor y Vinculación -->
<div class="row">
<div class="col-md-8 mb-3 form-group-animated">
<div class="col-md-8 mb-3">
<label for="valor_factura" class="form-label">Valor Factura</label>
<input id="valor_factura" name="valor_factura" type="number" step="0.01" min="0" class="form-control valor-total" required
value="<?= htmlspecialchars($factura['valor_factura']) ?>">
@@ -267,7 +193,7 @@
</div>
<div class="col-md-4 mb-3">
<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 no-animation no-border-style" accept="image/*">
<input id="foto_solicitud" name="foto_solicitud" type="file" class="form-control" accept="image/*">
</div>
</div>
@@ -293,9 +219,9 @@
<tbody>
<?php if (!empty($partidas)): foreach($partidas as $i=>$p): ?>
<tr>
<td><input name="partidas[<?= $i ?>][descripcion]" class="form-control form-group-animated" value="<?= htmlspecialchars($p['descripcion']) ?>"></td>
<td><input name="partidas[<?= $i ?>][cantidad_comercial]" type="number" step="0.0001" min="0" class="form-control form-group-animated" value="<?= htmlspecialchars($p['cantidad_comercial']) ?>"></td>
<td><input name="partidas[<?= $i ?>][cantidad_tarifa]" type="number" step="0.0001" min="0" class="form-control form-group-animated" value="<?= htmlspecialchars($p['cantidad_tarifa']) ?>"></td>
<td><input name="partidas[<?= $i ?>][descripcion]" class="form-control" value="<?= htmlspecialchars($p['descripcion']) ?>"></td>
<td><input name="partidas[<?= $i ?>][cantidad_comercial]" type="number" step="0.0001" min="0" class="form-control" value="<?= htmlspecialchars($p['cantidad_comercial']) ?>"></td>
<td><input name="partidas[<?= $i ?>][cantidad_tarifa]" type="number" step="0.0001" min="0" class="form-control" value="<?= htmlspecialchars($p['cantidad_tarifa']) ?>"></td>
<td>
<select name="partidas[<?= $i ?>][unidad_comercial_id]" class="form-select searchable">
<option value="">-- Unidad --</option>
@@ -306,8 +232,8 @@
<?php endforeach; ?>
</select>
</td>
<td><input name="partidas[<?= $i ?>][valor_factura]" type="number" step="0.01" min="0" class="form-control valor-partida form-group-animated" value="<?= htmlspecialchars($p['valor_factura']) ?>"></td>
<td><input name="partidas[<?= $i ?>][peso_bruto]" type="number" step="0.0001" min="0" class="form-control form-group-animated" value="<?= htmlspecialchars($p['peso_bruto']) ?>"></td>
<td><input name="partidas[<?= $i ?>][valor_factura]" type="number" step="0.01" min="0" class="form-control valor-partida" value="<?= htmlspecialchars($p['valor_factura']) ?>"></td>
<td><input name="partidas[<?= $i ?>][peso_bruto]" type="number" step="0.0001" min="0" class="form-control" value="<?= htmlspecialchars($p['peso_bruto']) ?>"></td>
<td>
<select name="partidas[<?= $i ?>][tasa_preferencial]" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -316,15 +242,15 @@
<?php endforeach; ?>
</select>
</td>
<td class="hide"><input name="partidas[<?= $i ?>][precio_unitario]" type="number" class="form-control form-group-animated" value="<?= htmlspecialchars($p['precio_unitario'] ?? '') ?>"></td>
<td class="hide"><input name="partidas[<?= $i ?>][oma_factura]" class="form-control form-group-animated" value="<?= htmlspecialchars($p['oma_factura'] ?? '') ?>"></td>
<td class="hide"><input name="partidas[<?= $i ?>][precio_unitario]" type="number" class="form-control" value="<?= htmlspecialchars($p['precio_unitario'] ?? '') ?>"></td>
<td class="hide"><input name="partidas[<?= $i ?>][oma_factura]" class="form-control" value="<?= htmlspecialchars($p['oma_factura'] ?? '') ?>"></td>
<td><button type="button" class="btn btn-danger btn-sm remove-row">✖️</button></td>
</tr>
<?php endforeach; else: ?>
<tr>
<td><input name="partidas[0][descripcion]" class="form-control form-group-animated"></td>
<td><input name="partidas[0][cantidad_comercial]" type="number" step="0.0001" class="form-control form-group-animated" min="0"></td>
<td><input name="partidas[0][cantidad_tarifa]" type="number" step="0.0001" class="form-control form-group-animated" min="0"></td>
<td><input name="partidas[0][descripcion]" class="form-control"></td>
<td><input name="partidas[0][cantidad_comercial]" type="number" step="0.0001" class="form-control" min="0"></td>
<td><input name="partidas[0][cantidad_tarifa]" type="number" step="0.0001" class="form-control" min="0"></td>
<td>
<select name="partidas[0][unidad_comercial_id]" class="form-select searchable">
<option value="">-- Unidad --</option>
@@ -333,8 +259,8 @@
<?php endforeach; ?>
</select>
</td>
<td><input name="partidas[0][valor_factura]" type="number" step="0.01" class="form-control valor-partida form-group-animated" min="0"></td>
<td><input name="partidas[0][peso_bruto]" type="number" step="0.0001" class="form-control form-group-animated" min="0"></td>
<td><input name="partidas[0][valor_factura]" type="number" step="0.01" class="form-control valor-partida" min="0"></td>
<td><input name="partidas[0][peso_bruto]" type="number" step="0.0001" class="form-control" min="0"></td>
<td>
<select name="partidas[0][tasa_preferencial]" class="form-select searchable">
<option value="">-- Selecciona --</option>
@@ -350,79 +276,75 @@
</tbody>
</table>
</div>
<div class="form-group-animated">
<button type="button" class="btn btn-secondary btn-sm mb-3 mt-auto w-auto btn-animated" id="add-partida"> Agregar partida</button>
</div>
<button type="button" class="btn btn-secondary btn-sm mb-3 mt-auto w-auto btn-animated" id="add-partida"> Agregar partida</button>
<!-- Activo -->
<div class="form-check mb-4 form-group-animated">
<div class="form-check mb-4">
<input id="status" value=1 name="status" type="hidden" <?= $factura['status']==1?'checked':'' ?>>
<label for="status" class="hide form-check-label">Activo</label>
</div>
<!-- Botones finales -->
<div class="col-12 text-end mt-4 form-group-animated"></div>
<button type="submit" class="btn btn-success mt-auto w-auto btn-animated">Actualizar</button>
<a href="/IMPORTADORES/solicitud_importacion/lista" class="btn btn-secondary ms-2 mt-auto w-auto btn-animated">Cancelar</a>
</div>
<button type="submit" class="btn btn-success mt-auto w-auto btn-animated">Actualizar</button>
<a href="/IMPORTADORES/solicitud_importacion/lista" class="btn btn-secondary ms-2 mt-auto w-auto btn-animated">Cancelar</a>
</form>
</div>
</div>
<!-- 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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
<script>
// ✅ 1. INICIALIZACIÓN PRINCIPAL
document.addEventListener('DOMContentLoaded', function() {
// Primero cargar proveedores, después inicializar otros selects
cargarProveedores().then(() => {
// Inicializar Choices.js en todos los selects searchable DESPUÉS de cargar proveedores
const proveedorActual = '<?= htmlspecialchars($proveedor_clave_actual ?? '') ?>';
console.log('📦 Proveedor actual:', proveedorActual);
cargarProveedores(proveedorActual).then(() => {
document.querySelectorAll('.searchable').forEach(el => {
new Choices(el, {
// Destruir instancia previa si existe
if (el.choicesInstance) el.choicesInstance.destroy();
const instance = new Choices(el, {
searchEnabled: true,
itemSelectText: '',
shouldSort: false
});
// Guardar instancia
el.choicesInstance = instance;
});
});
});
// ✅ 2. FUNCIÓN PARA CARGAR PROVEEDORES (CON PROMESA)
function cargarProveedores() {
const proveedorEl = document.getElementById('proveedor_clave');
// Obtenemos la cadena con la ID del proveedor guardado
const proveedorActual = '<?= htmlspecialchars($proveedor_clave_actual ?? '') ?>';
function cargarProveedores(proveedorActual) {
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 => {
// Limpiar el select
proveedorEl.innerHTML = '<option value="">-- Selecciona Proveedor --</option>';
// Agregar todas las opciones
if (json.results && json.results.length > 0) {
json.results.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = item.text;
json.results.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = item.text;
// Comparar forzando a cadena para que coincida con proveedorActual
if (String(item.id) === proveedorActual && proveedorActual !== '') {
opt.selected = true;
}
if (String(item.id) === proveedorActual) {
opt.selected = true;
}
proveedorEl.appendChild(opt);
});
}
proveedorEl.appendChild(opt);
});
// Forzar manualmente el value
proveedorEl.value = proveedorActual;
console.log('✅ Proveedores cargados. Proveedor actual:', proveedorActual);
console.log('🧪 Select value actual (después de asignar):', proveedorEl.value);
})
.catch(err => {
console.error('❌ Error cargando proveedores:', err);
@@ -433,8 +355,8 @@
// ✅ 3. AGREGAR PARTIDAS
document.getElementById('add-partida').addEventListener('click', () => {
const tbody = document.querySelector('#tabla-partidas tbody');
const idx = tbody.querySelectorAll('tr').length;
const row = document.createElement('tr');
const idx = tbody.querySelectorAll('tr').length;
const row = document.createElement('tr');
row.innerHTML = `
<td><input name="partidas[${idx}][descripcion]" class="form-control"></td>
@@ -453,7 +375,11 @@
<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>
<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>
@@ -716,67 +642,6 @@
}
});
});
// Script para manejar las animaciones de validación
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input.form-control, select.form_select');
const selects = document.querySelectorAll('select.form_select');
inputs.forEach(input => {
// Validación en tiempo real
input.addEventListener('input', function() {
if (this.checkValidity()) {
this.classList.remove('shake');
this.classList.add('valid');
} else {
this.classList.remove('valid');
}
});
// Efecto shake en campos inválidos
input.addEventListener('invalid', function() {
this.classList.add('shake');
setTimeout(() => {
this.classList.remove('shake');
}, 500);
});
});
// Configurar selects
selects.forEach(select => {
select.addEventListener('change', function() {
updateSelectState(this);
});
updateSelectState(select);
});
function updateSelectState(select) {
select.setAttribute('value', select.value);
if (select.value && select.value !== '') {
select.classList.add('valid');
select.classList.remove('invalid');
} else {
select.classList.remove('valid');
if (select.hasAttribute('required') && select.closest('form')?.classList.contains('was-validated')) {
select.classList.add('invalid');
}
}
}
// Validación del formulario
document.getElementById('solicitudForm').addEventListener('submit', function(e) {
e.preventDefault();
let isValid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
input.classList.add('shake');
isValid = false;
}
});
});
});
</script>
</body>

View File

@@ -333,9 +333,6 @@
// Aquí puedes enviar el formulario real
form.submit();
} else {
// Mostrar mensaje de error
showNotification('Por favor, complete todos los campos obligatorios correctamente.', 'error');
}
});
});