Files
MVE/app/controllers/solicitud_importacion.php
2025-07-15 14:50:39 -06:00

2038 lines
87 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/../helpers/session.php';
require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../helpers/crypto.php';
// 1) Composer autoload (phpdotenv y demás libs)
require_once __DIR__ . '/../../vendor/autoload.php';
// 2) Carga nuestro helper de entorno y dispara la carga de .env
require_once __DIR__ . '/../helpers/env.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use Dompdf\Dompdf;
use Dompdf\Options;
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$body");
$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$resp");
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;
}
/** Listado de solicitudes de importación (facturas) del importador logueado **/
function lista()
{
if (!($_SESSION['usuario_id'] ?? false)) {
header('Location: /IMPORTADORES/login');
exit;
}
$id_importador = $_SESSION['usuario_id'];
$conn = getConnection();
// ✅ NUEVO: Obtener la agencia actual del usuario
$stmt = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM dbo.usuarios_sistema WHERE id_usuario = ?", [$id_importador]);
$id_agencia = null;
if ($stmt && $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$id_agencia = $row['id_agencia_en_uso'];
}
$sql = "SELECT
f.*,
tr.nombre AS transportista,
(c.nombre + ' ' + c.apellido) AS chofer,
p.nombre AS nombre_pais_proveedor,
f.foto_solicitud_url,
a.patente AS patente
FROM dbo.solicitud_importacion_factura f
JOIN dbo.transportistas tr
ON f.transportista_id = tr.id_transportista
LEFT JOIN dbo.choferes c
ON f.chofer_id = c.id_chofer
LEFT JOIN dbo.paises p
ON f.pais_proveedor = p.id_pais
LEFT JOIN dbo.agentes_aduanales a
ON f.patente_id = a.id_agente
WHERE f.id_importador = ?
AND f.id_agencia = ?
AND f.status >= 1
ORDER BY f.created_at DESC
";
$stmt = sqlsrv_query($conn, $sql, [$id_importador, $id_agencia]);
if ($stmt === false) {
die("Error en lista(): " . print_r(sqlsrv_errors(), true));
}
$facturas = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
if ($row['fecha_factura'] instanceof DateTime) {
$row['fecha_factura'] = $row['fecha_factura']->format('Y-m-d');
}
$facturas[] = $row;
}
include __DIR__ . '/../../views/solicitud_importacion/lista.php';
}
/** Formulario de nueva factura **/
function crear()
{
if (!($_SESSION['usuario_id'] ?? false)) {
header('Location: /IMPORTADORES/login');
exit;
}
$id_importador = $_SESSION['usuario_id'];
$conn = getConnection();
// ✅ NUEVO: Obtener la agencia actual del usuario
$stmt = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM dbo.usuarios_sistema WHERE id_usuario = ?", [$id_importador]);
if ($stmt === false) { die(print_r(sqlsrv_errors(), true)); }
$id_agencia = null;
if ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { $id_agencia = $row['id_agencia_en_uso']; }
// Carga de datos para selects
$patentes = [];
$stmtP = sqlsrv_query($conn, "SELECT id_agente, patente, agente_aduanal FROM dbo.agentes_aduanales WHERE id_agencia = ? AND activo = 1 ORDER BY patente", [$id_agencia]);
while ($r = sqlsrv_fetch_array($stmtP, SQLSRV_FETCH_ASSOC)) { $patentes[] = $r; }
$transportistas = [];
$stmtT = sqlsrv_query($conn, "SELECT id_transportista, clave_identificador, nombre FROM dbo.transportistas WHERE id_usuario = ? AND activo = 1 ORDER BY nombre",[$id_importador]);
while ($r = sqlsrv_fetch_array($stmtT, SQLSRV_FETCH_ASSOC)) { $transportistas[] = $r; }
// ✅ MODIFICADO: Obtener choferes con su transportista_id para el filtro
$choferes = [];
$stmtC = sqlsrv_query($conn, "SELECT c.id_chofer, c.nombre+' '+c.apellido AS nombre, c.transportista_id FROM dbo.choferes c JOIN dbo.transportistas t ON c.transportista_id=t.id_transportista WHERE t.id_usuario=? AND c.status=1 ORDER BY c.nombre",[$id_importador]);
while ($r = sqlsrv_fetch_array($stmtC, SQLSRV_FETCH_ASSOC)) { $choferes[] = $r; }
$paises = [];
$stmtP = sqlsrv_query($conn, "SELECT id_pais, nombre FROM dbo.paises ORDER BY nombre");
while ($r = sqlsrv_fetch_array($stmtP, SQLSRV_FETCH_ASSOC)) { $paises[] = $r; }
$aduanas = [];
$stmtA = sqlsrv_query($conn, "SELECT DISTINCT RIGHT(REPLICATE('0', 3) + CAST(aduana_seccion AS VARCHAR), 3) AS aduana_seccion, nombre FROM dbo.aduanas ORDER BY aduana_seccion");
while ($r = sqlsrv_fetch_array($stmtA, SQLSRV_FETCH_ASSOC)) { $aduanas[] = $r; }
$incoterms = [];
$stmtI = sqlsrv_query($conn, "SELECT INCOTERM, DESCESPANOL FROM dbo.gIncoterms ORDER BY INCOTERM");
while ($r = sqlsrv_fetch_array($stmtI, SQLSRV_FETCH_ASSOC)) { $incoterms[] = $r; }
$unidades_medida = [];
$stmtU = sqlsrv_query($conn, "SELECT id, descripcion FROM dbo.unidades_medida_apendice7 ORDER BY id");
while ($r = sqlsrv_fetch_array($stmtU, SQLSRV_FETCH_ASSOC)) { $unidades_medida[] = $r; }
include __DIR__ . '/../../views/solicitud_importacion/crear.php';
}
// ✅ NUEVO: Endpoint AJAX para obtener choferes por transportista
function obtenerChoferesPorTransportista()
{
// Configurar headers para JSON desde el inicio
header('Content-Type: application/json');
try {
if (!($_SESSION['usuario_id'] ?? false)) {
http_response_code(401);
echo json_encode(['error' => 'No autorizado']);
exit;
}
$id_importador = $_SESSION['usuario_id'];
$transportista_id = $_GET['transportista_id'] ?? null;
if (!$transportista_id || !is_numeric($transportista_id)) {
http_response_code(400);
echo json_encode(['error' => 'ID de transportista requerido']);
exit;
}
$conn = getConnection();
// Validar que el transportista pertenezca al usuario
$stmtValidate = sqlsrv_query($conn,
"SELECT id_transportista FROM dbo.transportistas WHERE id_transportista = ? AND id_usuario = ? AND activo = 1",
[intval($transportista_id), $id_importador]);
if (!$stmtValidate || !sqlsrv_fetch_array($stmtValidate, SQLSRV_FETCH_ASSOC)) {
http_response_code(403);
echo json_encode(['error' => 'Transportista no válido']);
exit;
}
// Obtener choferes del transportista
$choferes = [];
$stmtC = sqlsrv_query($conn,
"SELECT c.id_chofer, c.nombre+' '+c.apellido AS nombre
FROM dbo.choferes c
WHERE c.transportista_id = ? AND c.status = 1
ORDER BY c.nombre",
[intval($transportista_id)]);
if ($stmtC) {
while ($r = sqlsrv_fetch_array($stmtC, SQLSRV_FETCH_ASSOC)) {
$choferes[] = [
'id_chofer' => $r['id_chofer'],
'nombre' => $r['nombre']
];
}
}
echo json_encode($choferes);
exit;
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor']);
exit;
}
}
/** Procesa la creación de una nueva factura y sus partidas **/
function guardar()
{
if (!($_SESSION['usuario_id'] ?? false)) {
die("⚠️ No autorizado.");
}
$id_importador = $_SESSION['usuario_id'];
$conn = getConnection();
// ✅ Obtener id_agencia_en_uso del usuario
$stmtAgencia = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM dbo.usuarios_sistema WHERE id_usuario = ?", [$id_importador]);
$id_agencia = null;
if ($stmtAgencia && $row = sqlsrv_fetch_array($stmtAgencia, SQLSRV_FETCH_ASSOC)) {
$id_agencia = $row['id_agencia_en_uso'];
}
if (!$id_agencia) {
die("❌ No se pudo determinar la agencia en uso para guardar la solicitud.");
}
// Obtener y validar campos del formulario
$aduana_seccion = $_POST['anexo22_apendice'] ?? null;
$num_factura = trim($_POST['numero_factura'] ?? '');
$fecha = $_POST['fecha_factura'] ?? null;
$incoterm = $_POST['incoterm'] ?? null;
$pais_proveedor = $_POST['pais_proveedor'] ?? null;
$tipo_moneda = $_POST['tipo_moneda'] ?? null;
$valor_factura = $_POST['valor_factura'] ?? null;
$vinculacion = $_POST['vinculacion'] ?? 0;
$transportista_id = $_POST['transportista_id'] ?? null;
$chofer_id = $_POST['chofer_id'] ?? null;
$status = isset($_POST['status']) ? 1 : 0;
$proveedor_clave = trim($_POST['proveedor_id'] ?? '');
$patente_id = $_POST['patente'] ?? null;
if ($patente_id) {
$stmtValidatePatente = sqlsrv_query($conn,
"SELECT id_agente FROM dbo.agentes_aduanales WHERE id_agente = ? AND id_agencia = ? AND activo = 1",
[$patente_id, $id_agencia]);
if (!$stmtValidatePatente || !sqlsrv_fetch_array($stmtValidatePatente, SQLSRV_FETCH_ASSOC)) {
die("❌ La patente seleccionada no es válida para su agencia.");
}
sqlsrv_free_stmt($stmtValidatePatente);
}
if (empty($num_factura) || empty($fecha) || empty($transportista_id) || empty($chofer_id)) {
die("❌ Faltan campos obligatorios.");
}
// Foto solicitud
$fotoUrl = null;
if (!empty($_FILES['foto_solicitud']['tmp_name']) && $_FILES['foto_solicitud']['error']===UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['foto_solicitud']['name'], PATHINFO_EXTENSION);
$dest = __DIR__.'/../../public/uploads/solicitud_'.uniqid().".$ext";
if (!is_dir(dirname($dest))) mkdir(dirname($dest),0755,true);
if(move_uploaded_file($_FILES['foto_solicitud']['tmp_name'],$dest)) {
$fotoUrl = "/IMPORTADORES/public/uploads/".basename($dest);
}
}
$params = [
$id_importador,
$id_agencia,
$aduana_seccion,
$aduana_seccion,
$num_factura,
$fecha,
$incoterm,
$pais_proveedor,
$tipo_moneda,
$valor_factura,
$vinculacion,
(int)$transportista_id,
(int)$chofer_id,
$fotoUrl,
$status,
$proveedor_clave,
$patente_id ? (int)$patente_id : null
];
$sql = "INSERT INTO dbo.solicitud_importacion_factura
(id_importador, id_agencia, aduana, anexo22_apendice, numero_factura,
fecha_factura, numero_pedimento, incoterm, pais_proveedor, tipo_moneda,
valor_factura, vinculacion, transportista_id, chofer_id,
foto_solicitud_url, status, proveedor_clave, patente_id)
OUTPUT INSERTED.id_solicitud
VALUES(?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
";
$stmt = sqlsrv_query($conn, $sql, $params, ['Scrollable' => SQLSRV_CURSOR_KEYSET]);
if ($stmt === false) {
die("❌ Error ejecutando INSERT con OUTPUT: " . print_r(sqlsrv_errors(), true));
}
$new = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if (!$new || empty($new['id_solicitud'])) {
die("❌ No se pudo recuperar el ID insertado de la factura.");
}
$id_solicitud = (int)$new['id_solicitud'];
try {
// Verificar configuración de notificaciones del usuario
$sqlNotif = "SELECT
u.nombre, u.email, u.notificaciones, u.notificaciones_extra,
COALESCE(p.nuevas_solicitudes, 0) as nuevas_solicitudes,
ce.correo as correo_extra
FROM usuarios_sistema u
LEFT JOIN preferencias_notificaciones_usuario p
ON u.id_usuario = p.id_usuario
LEFT JOIN correo_extra ce
ON u.id_usuario = ce.id_usuario
WHERE u.id_usuario = ?
";
$stmtNotif = sqlsrv_prepare($conn, $sqlNotif, [$id_importador]);
if ($stmtNotif && sqlsrv_execute($stmtNotif)) {
$notifConfig = sqlsrv_fetch_array($stmtNotif, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmtNotif);
// 🔐 Desencriptar datos
if ($notifConfig) {
// Correo
$notifConfig['email'] = decrypt($notifConfig['email']);
// Nombre
$notifConfig['nombre'] = decrypt($notifConfig['nombre']);
}
// Verificar si debe enviar notificaciones
if ($notifConfig &&
$notifConfig['notificaciones'] == 1 &&
$notifConfig['nuevas_solicitudes'] == 1) {
// Preparar datos para la notificación
$datosNotificacion = [
'id_solicitud' => $id_solicitud,
'numero_factura' => $num_factura,
'fecha_factura' => $fecha,
'valor_factura' => $valor_factura,
'tipo_moneda' => $tipo_moneda
];
// Enviar al correo principal
$resultadoPrincipal = enviarNotificacionNuevaSolicitud(
$notifConfig['email'],
$notifConfig['nombre'],
$datosNotificacion
);
// Enviar al correo adicional si está configurado
if ($notifConfig['notificaciones_extra'] == 1 && !empty($notifConfig['correo_extra'])) {
$resultadoExtra = enviarNotificacionNuevaSolicitud(
$notifConfig['correo_extra'],
$notifConfig['nombre'],
$datosNotificacion,
true // Indicar que es correo adicional
);
}
// Log de notificaciones enviadas (opcional)
error_log("✅ Notificación enviada para solicitud ID: $id_solicitud");
} else {
// Log informativo
error_log(" Usuario ID: $id_importador no tiene notificaciones de nuevas solicitudes habilitadas");
}
} else {
error_log("⚠️ No se pudo consultar configuración de notificaciones para usuario ID: $id_importador");
}
} catch (Exception $e) {
// No fallar el proceso por errores de notificación
error_log("❌ Error en sistema de notificaciones: " . $e->getMessage());
}
// Partidas
if(!empty($_POST['partidas'])&&is_array($_POST['partidas'])){
$sqlP = "INSERT INTO dbo.solicitud_importacion_partidas
(id_solicitud, descripcion, cantidad_comercial, cantidad_tarifa, valor_factura, peso_bruto, unidad_comercial_id, tasa_preferencial)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$partidas_insertadas = 0; // ← contador
foreach ($_POST['partidas'] as $i => $p) {
error_log("Partida $i: " . print_r($p, true));
$params = [
$id_solicitud,
trim($p['descripcion'] ?? ''),
floatval($p['cantidad_comercial'] ?? 0),
floatval($p['cantidad_tarifa'] ?? 0),
floatval($p['valor_factura'] ?? 0),
floatval($p['peso_bruto'] ?? 0),
intval($p['unidad_comercial_id'] ?? 0) ?: null,
trim($p['tasa_preferencial'] ?? '')
];
if ($params[1] !== '' && $params[2] > 0) {
$stmtPartida = sqlsrv_query($conn, $sqlP, $params);
if ($stmtPartida === false) {
die("❌ Error insertando partida $i: " . print_r(sqlsrv_errors(), true));
} else {
$partidas_insertadas++;
}
}
}
if ($partidas_insertadas === 0) {
///header('Location: /IMPORTADORES/solicitud_importacion/crear?error_partidas=1');
// exit;
}
}
header('Location: /IMPORTADORES/solicitud_importacion/lista?created=ok');
exit;
}
/** Función para generar la notificación **/
function enviarNotificacionNuevaSolicitud($email, $nombreUsuario, $datosSolicitud, $esCorreoExtra = false)
{
$mail = new PHPMailer(true);
try {
// Configuración SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'] ?? '';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->CharSet = 'UTF-8';
// Configuración del mensaje
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($email);
$mail->isHTML(true);
// Personalizar subject si es correo adicional
$subjectPrefix = $esCorreoExtra ? '[COPIA] ' : '';
$mail->Subject = $subjectPrefix . '📦 Nueva Solicitud de Importación Registrada';
// Formatear datos para el email
$idSolicitud = $datosSolicitud['id_solicitud'];
$numeroFactura = htmlspecialchars($datosSolicitud['numero_factura']);
$fechaFactura = htmlspecialchars($datosSolicitud['fecha_factura']);
$valorFactura = number_format($datosSolicitud['valor_factura'], 2);
$tipoMoneda = htmlspecialchars($datosSolicitud['tipo_moneda']);
$tipoNotificacion = $esCorreoExtra ?
'<div style="background: #fff3cd; padding: 10px; border-radius: 5px; margin-bottom: 15px; border-left: 4px solid #ffc107;">
<small><strong>📧 Copia enviada a correo adicional</strong></small>
</div>' : '';
$mail->Body = "
<div style='font-family: Segoe UI, sans-serif; background-color: #f4f6f9; padding: 30px;'>
<div style='max-width: 600px; margin: auto; background: #fff; border: 1px solid #ccc; border-radius: 10px;'>
<div style='background: linear-gradient(to right, #003366, #0055A5); padding: 20px; text-align: center;'>
<h2 style='color: white; margin: 0;'>📥 Nueva Solicitud Registrada</h2>
</div>
<div style='padding: 20px;'>
$tipoNotificacion
<p>Hola <strong>" . htmlspecialchars($nombreUsuario) . "</strong>,</p>
<p>Tu solicitud de importación ha sido registrada correctamente con los siguientes datos:</p>
<div style='background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 15px 0;'>
<table style='width: 100%; border-collapse: collapse;'>
<tr>
<td style='padding: 5px 0; font-weight: bold;'>ID Solicitud:</td>
<td style='padding: 5px 0;'>$idSolicitud</td>
</tr>
<tr>
<td style='padding: 5px 0; font-weight: bold;'>Número de Factura:</td>
<td style='padding: 5px 0;'>$numeroFactura</td>
</tr>
<tr>
<td style='padding: 5px 0; font-weight: bold;'>Fecha:</td>
<td style='padding: 5px 0;'>$fechaFactura</td>
</tr>
<tr>
<td style='padding: 5px 0; font-weight: bold;'>Valor:</td>
<td style='padding: 5px 0;'>$valorFactura $tipoMoneda</td>
</tr>
</table>
</div>
<p>Puedes consultar el estado de tu solicitud accediendo a tu panel de control.</p>
<br>
<p style='color: #888; font-size: 14px;'>Si no realizaste esta acción, contacta al administrador del sistema.</p>
</div>
<div style='background: #e9ecef; text-align: center; padding: 10px; font-size: 13px; color: #666;'>
&copy; " . date('Y') . " SIIH · Desarrollado por AduanaSoft
</div>
</div>
</div>";
$envioExitoso = $mail->send();
if ($envioExitoso) {
$tipoCorreo = $esCorreoExtra ? 'correo adicional' : 'correo principal';
error_log("✅ Notificación enviada exitosamente al $tipoCorreo: $email");
return true;
}
return false;
} catch (Exception $e) {
$tipoCorreo = $esCorreoExtra ? 'correo adicional' : 'correo principal';
error_log("❌ Error al enviar notificación al $tipoCorreo ($email): {$mail->ErrorInfo}");
return false;
}
}
/** Formulario de edición de factura **/
function editar()
{
if (!($_SESSION['usuario_id'] ?? false)) {
header('Location: /IMPORTADORES/login');
exit;
}
$id_solicitud = $_GET['id']??null;
if(!$id_solicitud || !is_numeric($id_solicitud)) {
die("❌ ID inválido.");
}
$id_importador = $_SESSION['usuario_id'];
$conn = getConnection();
// ✅ NUEVO: Obtener la agencia actual del usuario
$stmtAg = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM dbo.usuarios_sistema WHERE id_usuario = ?", [$id_importador]);
if ($stmtAg === false) { die(print_r(sqlsrv_errors(), true)); }
$id_agencia = null;
if ($row = sqlsrv_fetch_array($stmtAg, SQLSRV_FETCH_ASSOC)) { $id_agencia = $row['id_agencia_en_uso']; }
$stmt = sqlsrv_query($conn, "SELECT * FROM dbo.solicitud_importacion_factura WHERE id_solicitud=? AND id_importador=? AND id_agencia=?", [(int)$id_solicitud, $id_importador, $id_agencia]);
if($stmt === false) {
die(print_r(sqlsrv_errors(),true));
}
$factura = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if(!$factura) {
die("❌ No autorizado.");
}
if($factura['fecha_factura'] instanceof DateTime) {
$factura['fecha_factura'] = $factura['fecha_factura']->format('Y-m-d');
}
// Solo necesitamos la clave del proveedor actual para JavaScript
$proveedor_clave_actual = $factura['proveedor_clave'] ?? '';
// Carga selects (igual que crear)
// Patentes
$patentes = [];
$stmtP = sqlsrv_query($conn, "SELECT id_agente, patente, agente_aduanal FROM dbo.agentes_aduanales WHERE id_agencia = ? AND activo = 1 ORDER BY patente", [$id_agencia]);
while ($r = sqlsrv_fetch_array($stmtP, SQLSRV_FETCH_ASSOC)) { $patentes[] = $r; }
// Transportistas
$transportistas = [];
$stmtT = sqlsrv_query($conn, "SELECT id_transportista, clave_identificador, nombre FROM dbo.transportistas WHERE id_usuario = ? AND activo = 1 ORDER BY nombre",[$id_importador]);
while ($r = sqlsrv_fetch_array($stmtT, SQLSRV_FETCH_ASSOC)) { $transportistas[] = $r; }
// Choferes
$choferes = [];
$stmtC = sqlsrv_query($conn, "SELECT c.id_chofer, c.nombre+' '+c.apellido AS nombre, c.transportista_id FROM dbo.choferes c JOIN dbo.transportistas t ON c.transportista_id = t.id_transportista
WHERE t.id_usuario=? AND c.status=1 ORDER BY c.nombre",[$id_importador]);
while ($r = sqlsrv_fetch_array($stmtC, SQLSRV_FETCH_ASSOC)) { $choferes[] = $r; }
// Paises
$paises = [];
$stmtP = sqlsrv_query($conn, "SELECT id_pais, nombre FROM dbo.paises ORDER BY nombre");
while ($r = sqlsrv_fetch_array($stmtP, SQLSRV_FETCH_ASSOC)) { $paises[] = $r;}
// Aduanas
$aduanas = [];
$stmtA = sqlsrv_query($conn, "SELECT DISTINCT RIGHT(REPLICATE('0', 3) + CAST(aduana_seccion AS VARCHAR), 3) AS aduana_seccion, nombre FROM dbo.aduanas ORDER BY aduana_seccion");
while ($r = sqlsrv_fetch_array($stmtA, SQLSRV_FETCH_ASSOC)) { $aduanas[] = $r; }
// Incoterms
$incoterms = [];
$stmtI = sqlsrv_query($conn, "SELECT INCOTERM, DESCESPANOL FROM dbo.gIncoterms ORDER BY INCOTERM");
while ($r = sqlsrv_fetch_array($stmtI, SQLSRV_FETCH_ASSOC)) { $incoterms[] = $r; }
// Unidades de Medida
$unidades_medida = [];
$stmtU = sqlsrv_query($conn, "SELECT id, descripcion FROM dbo.unidades_medida_apendice7 ORDER BY id");
while ($r = sqlsrv_fetch_array($stmtU, SQLSRV_FETCH_ASSOC)) { $unidades_medida[] = $r; }
// Partidas existentes
$partidas = [];
$stmtPar = sqlsrv_query($conn, "SELECT id_partida, descripcion, cantidad_comercial, cantidad_tarifa, valor_factura, peso_bruto, unidad_comercial_id, tasa_preferencial
FROM dbo.solicitud_importacion_partidas WHERE id_solicitud = ? ORDER BY id_partida", [(int)$id_solicitud]);
while ($r = sqlsrv_fetch_array($stmtPar, SQLSRV_FETCH_ASSOC)) { $partidas[]=$r; }
include __DIR__ . '/../../views/solicitud_importacion/editar.php';
}
/** Procesa la actualización de una factura y sus partidas **/
function actualizar()
{
if (!($_SESSION['usuario_id'] ?? false)) {
die("⚠️ No autorizado.");
}
$id_importador = $_SESSION['usuario_id'];
$id_solicitud = (int)($_POST['id_solicitud'] ?? 0);
if ($id_solicitud <= 0) {
die("❌ ID inválido.");
}
// 1) Conexión
$conn = getConnection();
// ✅ NUEVO: Obtener la agencia actual del usuario
$stmt = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM dbo.usuarios_sistema WHERE id_usuario = ?", [$id_importador]);
$id_agencia = null;
if ($stmt && $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$id_agencia = $row['id_agencia_en_uso'];
}
// 2) Obtener URL de foto actual desde BD para conservar si no suben nueva
$fotoUrl = null;
$stmtFoto = sqlsrv_query($conn, "SELECT foto_solicitud_url FROM dbo.solicitud_importacion_factura WHERE id_solicitud = ? AND id_importador = ? AND id_agencia = ?", [ $id_solicitud, $_SESSION['usuario_id'], $id_agencia ] );
if ($stmtFoto !== false && ($row = sqlsrv_fetch_array($stmtFoto, SQLSRV_FETCH_ASSOC))) {
$fotoUrl = $row['foto_solicitud_url'];
}
// 3) Procesar posible nueva foto
if (!empty($_FILES['foto_solicitud']['tmp_name']) && $_FILES['foto_solicitud']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['foto_solicitud']['name'], PATHINFO_EXTENSION);
$dest = __DIR__ . '/../../public/uploads/solicitud_' . uniqid() . ".$ext";
if (!is_dir(dirname($dest))) { mkdir(dirname($dest), 0755, true); }
if (move_uploaded_file($_FILES['foto_solicitud']['tmp_name'], $dest)) { $fotoUrl = "/IMPORTADORES/public/uploads/" . basename($dest); }
}
// 4) Extraer campos del formulario
$aduana_seccion = $_POST['anexo22_apendice'] ?? null;
$num_factura = trim($_POST['numero_factura'] ?? '');
$fecha = $_POST['fecha_factura'] ?? null;
$incoterm = $_POST['incoterm'] ?? null;
$pais_proveedor = $_POST['pais_proveedor'] ?? null;
$tipo_moneda = $_POST['tipo_moneda'] ?? null;
$valor_factura = $_POST['valor_factura'] ?? null;
$vinculacion = $_POST['vinculacion'] ?? 0;
$transportista_id = (int)($_POST['transportista_id'] ?? 0);
$chofer_id = (int)($_POST['chofer_id'] ?? 0);
$status = isset($_POST['status']) ?? 1;
$patente_id = $_POST['patente'] ?? null;
$proveedor_clave = trim($_POST['proveedor_clave'] ?? null);
if ($proveedor_clave === '') {
$proveedor_clave = null;
}
// ✅ NUEVO: Validar que la patente pertenezca a la agencia del usuario (si se seleccionó una)
if ($patente_id) {
$stmtValidatePatente = sqlsrv_query($conn, "SELECT id_agente FROM dbo.agentes_aduanales WHERE id_agente = ? AND id_agencia = ? AND activo = 1", [$patente_id, $id_agencia]);
if (!$stmtValidatePatente || !sqlsrv_fetch_array($stmtValidatePatente, SQLSRV_FETCH_ASSOC)) {
die("❌ La patente seleccionada no es válida para su agencia.");
}
sqlsrv_free_stmt($stmtValidatePatente);
}
// 5) Validar obligatorios
if (empty($num_factura) || empty($fecha) || $transportista_id <= 0 || $chofer_id <= 0) {
die("❌ Faltan campos obligatorios.");
}
// 6) UPDATE de la cabecera
$paramsU = [
$aduana_seccion,
$aduana_seccion,
$num_factura,
$fecha,
$incoterm,
$pais_proveedor,
$tipo_moneda,
$valor_factura,
$vinculacion,
(int)$transportista_id,
(int)$chofer_id,
$fotoUrl,
$status,
$proveedor_clave,
$patente_id ? (int)$patente_id : null,
$id_solicitud,
$_SESSION['usuario_id'],
$id_agencia
];
$sqlU = "UPDATE dbo.solicitud_importacion_factura SET
aduana = ?,
anexo22_apendice = ?,
numero_factura = ?,
fecha_factura = ?,
incoterm = ?,
pais_proveedor = ?,
tipo_moneda = ?,
valor_factura = ?,
vinculacion = ?,
transportista_id = ?,
chofer_id = ?,
foto_solicitud_url = ?,
status = ?,
proveedor_clave = ?,
patente_id = ?,
updated_at = GETDATE()
WHERE id_solicitud = ?
AND id_importador = ?
AND id_agencia = ?
";
$stmtU = sqlsrv_query($conn, $sqlU, $paramsU);
if ($stmtU === false) {
die("❌ Error ejecutando UPDATE: " . print_r(sqlsrv_errors(), true));
}
// 7) Borrar partidas anteriores
$del = sqlsrv_query($conn, "DELETE FROM dbo.solicitud_importacion_partidas WHERE id_solicitud = ?", [ $id_solicitud ]);
if ($del === false) {
die("❌ Error borrando partidas previas: " . print_r(sqlsrv_errors(), true));
}
// 8) Reinsertar partidas desde el formulario
if (!empty($_POST['partidas']) && is_array($_POST['partidas'])) {
$sqlP = "INSERT INTO dbo.solicitud_importacion_partidas
(id_solicitud, descripcion, cantidad_comercial, cantidad_tarifa,
valor_factura, peso_bruto, unidad_comercial_id, tasa_preferencial)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
";
foreach ($_POST['partidas'] as $i => $p) {
$desc = trim($p['descripcion'] ?? '');
$cantCom = floatval($p['cantidad_comercial'] ?? 0);
$cantTar = floatval($p['cantidad_tarifa'] ?? 0);
$valPart = floatval($p['valor_factura'] ?? 0);
$peso = floatval($p['peso_bruto'] ?? 0);
$umId = intval($p['unidad_comercial_id'] ?? 0) ?: null;
$tasaPref = trim($p['tasa_preferencial'] ?? '');
// Sólo inserta si descripción y cantidad comercial válidos
if ($desc !== '' && $cantCom > 0) {
$paramsP = [
$id_solicitud,
$desc,
$cantCom,
$cantTar,
$valPart,
$peso,
$umId,
$tasaPref
];
$stmtP = sqlsrv_query($conn, $sqlP, $paramsP);
if ($stmtP === false) {
die("❌ Error insertando partida #$i: " . print_r(sqlsrv_errors(), true));
}
}
}
}
// 9) Redirigir
header('Location: /IMPORTADORES/solicitud_importacion/lista?updated=ok');
exit;
}
/** “Soft-delete” de una factura **/
function eliminar()
{
if (!($_SESSION['usuario_id'] ?? false)) {
header('Location: /IMPORTADORES/login');
exit;
}
$id = (int) ($_GET['id'] ?? 0);
$conn = getConnection();
sqlsrv_query($conn, "UPDATE dbo.solicitud_importacion_factura SET status = 0, updated_at = GETDATE() WHERE id_solicitud = ? AND id_importador = ?",[$id,$_SESSION['usuario_id']]);
header('Location: /IMPORTADORES/solicitud_importacion/lista?deleted=ok'); exit;
}
function actualizar_masivo()
{
header('Content-Type: application/json; charset=utf-8');
if (!($_SESSION['usuario_id'] ?? false)) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
exit;
}
$ids = $_POST['ids'] ?? [];
$status = (int) ($_POST['status'] ?? 0);
if (empty($ids) || !is_array($ids)) {
echo json_encode(['success' => false, 'error' => 'No se recibieron solicitudes']);
exit;
}
$conn = getConnection();
$usuarioId = $_SESSION['usuario_id'];
$token = ($status === 2) ? getApiToken() : null;
$actualizadas = 0;
$fallidas = [];
$pedimentosGenerados = [];
foreach ($ids as $id) {
$id = (int)$id;
// 1. Actualizar el status
$sql = "UPDATE dbo.solicitud_importacion_factura SET status = ? WHERE id_solicitud = ? AND id_importador = ?";
$stmt = sqlsrv_query($conn, $sql, [$status, $id, $usuarioId]);
if ($stmt === false) {
$fallidas[] = $id;
continue;
}
$actualizadas++;
// 2. Si el status es 2, generar pedimento
if ($status === 2 && $token) {
$res = generarPedimentoDesdeSolicitud($conn, $id, $usuarioId, $token);
if ($res['success']) {
$pedimentosGenerados[$id] = $res['numeroPedimento'] ?? null;
} else {
error_log("[actualizar_masivo] Error en pedimento solicitud $id: {$res['error']}");
}
}
}
echo json_encode([
'success' => true,
'actualizadas' => $actualizadas,
'fallidas' => $fallidas,
'pedimentos' => $pedimentosGenerados
]);
exit;
}
function generarPedimentoDesdeSolicitud($conn, $id, $usuarioId, $token)
{
try {
// Consulta solicitud
$sqlSel = "SELECT *, '9999' as patente FROM dbo.solicitud_importacion_factura WHERE id_solicitud = ? AND id_importador = ?";
$stmtSel = sqlsrv_query($conn, $sqlSel, [$id, $usuarioId]);
$solicitud = sqlsrv_fetch_array($stmtSel, SQLSRV_FETCH_ASSOC);
if (!$solicitud) return ['success' => false, 'error' => 'Solicitud no encontrada'];
// Fechas
$solicitud['fecha_factura'] = $solicitud['fecha_factura'] instanceof DateTime ? $solicitud['fecha_factura'] ->format('Y-m-d') : $solicitud['fecha_factura'];
$solicitud['created_at'] = $solicitud['created_at'] instanceof DateTime ? $solicitud['created_at'] ->format('Y-m-d\TH:i:s') : $solicitud['created_at'];
$solicitud['updated_at'] = $solicitud['updated_at'] instanceof DateTime ? $solicitud['updated_at'] ->format('Y-m-d\TH:i:s') : $solicitud['updated_at'];
// Partidas
$partidas = [];
$stmtPart = sqlsrv_query($conn, "SELECT * FROM dbo.solicitud_importacion_partidas WHERE id_solicitud = ?", [$id]);
while ($p = sqlsrv_fetch_array($stmtPart, SQLSRV_FETCH_ASSOC)) {
if ($p['creado_en'] instanceof DateTime) $p['creado_en'] = $p['creado_en']->format('Y-m-d\TH:i:s');
$p['cantidad_comercial'] = (float)$p['cantidad_comercial'];
$p['cantidad_tarifa'] = (float)$p['cantidad_tarifa'];
$p['valor_factura'] = (float)$p['valor_factura'];
$p['peso_bruto'] = (float)$p['peso_bruto'];
$p['unidad_comercial_id'] = (int)$p['unidad_comercial_id'];
$partidas[] = $p;
}
$solicitud['numero_pedimento'] = "";
$solicitud['transporte_id'] = null;
$solicitud['partidas'] = $partidas;
$jsonPayload = json_encode($solicitud);
$apiBase = rtrim($_ENV['API_URL'] ?? '', '/');
$urlPed = $apiBase . '/pedimentos/crearPedimento';
$ch = curl_init($urlPed);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $jsonPayload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$respPed = curl_exec($ch);
$httpPed = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpPed === 200 || $httpPed === 201) {
$decoded = json_decode($respPed, true);
if (isset($decoded['pedimento']['PEDIMENTO'])) {
$numeroPed = $decoded['pedimento']['PEDIMENTO'];
sqlsrv_query($conn, "UPDATE dbo.solicitud_importacion_factura SET numero_pedimento = ? WHERE id_solicitud = ?", [$numeroPed, $id]);
return ['success' => true, 'numeroPedimento' => $numeroPed];
}
return ['success' => false, 'error' => 'Respuesta inválida de API'];
}
return ['success' => false, 'error' => "HTTP $httpPed"];
} catch (Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
/** GET /IMPORTADORES/solicitud_importacion/ajax_proveedores
* Devuelve JSON para poblar el select de Proveedor **/
function ajax_proveedores()
{
// 1) Asegura que la respuesta sea JSON
header('Content-Type: application/json; charset=utf-8');
// 2) Obtén o renueva tu token (usa tu función existente)
$token = getApiToken();
if (!$token) {
echo json_encode(['results' => []]);
return;
}
// 3) Llama al endpoint de la API de proveedores
$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);
// 4) Parsear y transformar al formato { results: [ {id,text}, … ] }
$out = ['results' => []];
if ($status === 200 && ($json = json_decode($resp, true)) && is_array($json)) {
foreach ($json as $p) {
$clave = $p['Clave'] ?? '';
$nombre = $p['Nombre'] ?? '';
$out['results'][] = [
'id' => $clave,
'text' => "[$clave] - $nombre"
];
}
}
// 5) Devolver JSON
echo json_encode($out);
exit;
}
function ajax_lista()
{
// 1) Fijamos el header JSON
header('Content-Type: application/json; charset=utf-8');
// 2) Obtener o renovar token
$token = getApiToken();
if (!$token) {
error_log('[ajax_lista] Sin token válido');
// Devolvemos estructura vacía
echo json_encode(['data' => []]);
return;
}
// 3) Construimos la URL de la API
$apiBase = rtrim($_ENV['API_URL'] ?? '', '/');
$url = $apiBase . '/proveedores';
error_log("[ajax_lista] GET $url");
// 4) Ejecutamos cURL
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("[ajax_lista] HTTP $status$resp");
// 5) Parseamos y formateamos
$dataList = [];
if ($status === 200 && ($json = json_decode($resp, true)) && is_array($json)) {
foreach ($json as $p) {
$clave = htmlspecialchars($p['Clave'] ?? '', ENT_QUOTES);
// Construir dirección
$direccion = trim(implode(', ', array_filter([
$p['Calles'] ?? '',
'Num. Ext: ' . ($p['NumExt'] ?? ''),
'Num. Int: ' . ($p['NumInt'] ?? ''),
$p['Colonia'] ?? '',
$p['Municipio'] ?? '',
$p['Ciudad'] ?? '',
'C.P. ' . ($p['CodigoPostal'] ?? ''),
$p['EntidadFederativa'] ?? '',
$p['Pais'] ?? ''
])));
$dataList[] = [
$clave,
htmlspecialchars($p['Nombre'] ?? '', ENT_QUOTES),
htmlspecialchars($p['RFC'] ?? '', ENT_QUOTES),
htmlspecialchars($p['Ciudad'] ?? '', ENT_QUOTES),
htmlspecialchars($p['Telefono']?? '', ENT_QUOTES),
htmlspecialchars($direccion ?? '', ENT_QUOTES),
// Acciones
"<a href=\"/IMPORTADORES/proveedores/editar?clave=" . rawurlencode($clave) . "\" class=\"btn btn-sm btn-primary\">✏️</a>
<button class=\"btn btn-sm btn-danger\" onclick=\"confirmDelete('{$clave}')\">🗑️</button>"
];
}
} else {
error_log('[ajax_lista] Respuesta inválida o status != 200');
}
// 6) Devolvemos siempre HTTP 200 con data (posiblemente vacío)
echo json_encode(['data' => $dataList]);
}
function update_status()
{
header('Content-Type: application/json; charset=utf-8');
if (!($_SESSION['usuario_id'] ?? false)) {
http_response_code(401);
echo json_encode(['error' => 'No autorizado']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$status = intval($_POST['status'] ?? 0);
// 1) Conexión a la BD
$conn = getConnection();
// 2) Actualizar el status en la tabla solicitud_importacion_factura
$sql = "UPDATE dbo.solicitud_importacion_factura SET status = ? WHERE id_solicitud=? AND id_importador=?";
$params = [$status, $id, $_SESSION['usuario_id']];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
http_response_code(500);
echo json_encode(['error' => 'Error al actualizar status']);
exit;
}
// 3) Obtener datos del usuario y solicitud para notificación
try {
$sqlNotif = "SELECT
u.nombre, u.email, u.notificaciones, u.notificaciones_extra,
COALESCE(p.cambio_estado, 0) as cambio_estado,
ce.correo as correo_extra,
s.numero_factura, s.fecha_factura, s.valor_factura, s.tipo_moneda
FROM usuarios_sistema u
LEFT JOIN preferencias_notificaciones_usuario p
ON u.id_usuario = p.id_usuario
LEFT JOIN correo_extra ce
ON u.id_usuario = ce.id_usuario
INNER JOIN solicitud_importacion_factura s
ON s.id_importador = u.id_usuario
WHERE u.id_usuario = ?
AND s.id_solicitud = ?
";
$stmtNotif = sqlsrv_prepare($conn, $sqlNotif, [$_SESSION['usuario_id'], $id]);
if ($stmtNotif && sqlsrv_execute($stmtNotif)) {
$notifConfig = sqlsrv_fetch_array($stmtNotif, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmtNotif);
// 🔐 Desencriptar datos
if ($notifConfig) {
$notifConfig['email'] = decrypt($notifConfig['email']);
$notifConfig['nombre'] = decrypt($notifConfig['nombre']);
}
// Verificar si debe enviar notificaciones de cambio de status
if ($notifConfig && $notifConfig['notificaciones'] == 1 && $notifConfig['cambio_estado'] == 1) {
// Formatear fecha si es DateTime
$fechaFactura = ($notifConfig['fecha_factura'] instanceof DateTime)
? $notifConfig['fecha_factura']->format('Y-m-d')
: $notifConfig['fecha_factura'];
// Preparar datos para la notificación
$datosSolicitud = [
'id_solicitud' => $id,
'numero_factura' => $notifConfig['numero_factura'],
'fecha_factura' => $fechaFactura,
'valor_factura' => $notifConfig['valor_factura'],
'tipo_moneda' => $notifConfig['tipo_moneda'],
'nuevo_status' => $status
];
$notificacionesEnviadas = 0;
$erroresNotificacion = [];
// Enviar al correo principal
if (!empty($notifConfig['email']) && filter_var($notifConfig['email'], FILTER_VALIDATE_EMAIL)) {
$resultadoPrincipal = enviarNotificacionCambioStatus(
$notifConfig['email'],
$notifConfig['nombre'],
$datosSolicitud,
false // No es correo adicional
);
if ($resultadoPrincipal) {
$notificacionesEnviadas++;
} else {
$erroresNotificacion[] = 'correo principal';
}
}
// Enviar al correo adicional si está configurado
if ($notifConfig['notificaciones_extra'] == 1 &&
!empty($notifConfig['correo_extra']) &&
filter_var($notifConfig['correo_extra'], FILTER_VALIDATE_EMAIL)) {
$resultadoExtra = enviarNotificacionCambioStatus(
$notifConfig['correo_extra'],
$notifConfig['nombre'],
$datosSolicitud,
true // Es correo adicional
);
if ($resultadoExtra) {
$notificacionesEnviadas++;
} else {
$erroresNotificacion[] = 'correo adicional';
}
}
// Log consolidado del resultado
if ($notificacionesEnviadas > 0) {
error_log("✅ Notificaciones de cambio de status enviadas ($notificacionesEnviadas) para solicitud ID: $id → Status: $status");
}
if (!empty($erroresNotificacion)) {
error_log("⚠️ Errores al enviar notificaciones (" . implode(', ', $erroresNotificacion) . ") para solicitud ID: $id");
}
} else {
// Log informativo cuando las notificaciones están deshabilitadas
$razon = [];
if (!$notifConfig) {
$razon[] = 'configuración no encontrada';
} else {
if ($notifConfig['notificaciones'] != 1) $razon[] = 'notificaciones generales deshabilitadas';
if ($notifConfig['cambio_estado'] != 1) $razon[] = 'notificaciones de cambio de estado deshabilitadas';
}
error_log(" Usuario ID: {$_SESSION['usuario_id']} no recibirá notificación de cambio de status. Razón: " . implode(', ', $razon));
}
} else {
error_log("⚠️ No se pudo consultar configuración de notificaciones para usuario ID: {$_SESSION['usuario_id']} - Error SQL: " . print_r(sqlsrv_errors(), true));
}
} catch (Exception $e) {
// No fallar el proceso principal por errores de notificación
error_log("❌ Error en sistema de notificaciones de cambio de status para solicitud ID: $id - " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
}
// 4) Si el status actualizado es 2, hacer POST a /pedimentos/crearPedimento
if ($status === 2) {
// 4.1) Obtener o renovar el token de la API
$token = getApiToken();
if (!$token) {
error_log("[update_status] No se pudo obtener token para crearPedimento");
echo json_encode(['success' => true, 'warning' => 'No se generó pedimento: sin token']);
exit;
}
// 4.2) Leer de la BD todos los campos de la cabecera de la solicitud
$sqlSel = "SELECT
s.id_solicitud, s.id_importador, s.aduana, s.numero_pedimento, s.anexo22_apendice,
s.numero_factura, s.fecha_factura, s.incoterm, s.pais_proveedor,
s.tipo_moneda, s.valor_factura, s.vinculacion, s.transportista_id, s.chofer_id,
s.foto_solicitud_url, s.proveedor_clave, s.created_at, s.updated_at,
'9999' as patente
FROM dbo.solicitud_importacion_factura s
WHERE s.id_solicitud = ?
AND s.id_importador = ?
";
$stmtSel = sqlsrv_query($conn, $sqlSel, [$id, $_SESSION['usuario_id']]);
if ($stmtSel === false) {
error_log("[update_status] Error al consultar solicitud: " . print_r(sqlsrv_errors(), true));
echo json_encode(['success' => true, 'warning' => 'Status actualizado, fallo al leer datos para pedimento']);
exit;
}
$solicitud = sqlsrv_fetch_array($stmtSel, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmtSel);
if (!$solicitud) {
error_log("[update_status] No se encontró la solicitud para id={$id}");
echo json_encode(['success' => true, 'warning' => 'Status actualizado, solicitud no encontrada para pedimento']);
exit;
}
// 4.3) Formatear fechas (si vienen como DateTime) antes de construir JSON
$fechaFactura = ($solicitud['fecha_factura'] instanceof DateTime)
? $solicitud['fecha_factura']->format('Y-m-d')
: $solicitud['fecha_factura'];
$createdAt = ($solicitud['created_at'] instanceof DateTime)
? $solicitud['created_at']->format('Y-m-d\TH:i:s')
: $solicitud['created_at'];
$updatedAt = ($solicitud['updated_at'] instanceof DateTime)
? $solicitud['updated_at']->format('Y-m-d\TH:i:s')
: $solicitud['updated_at'];
// 4.4) Obtener todas las partidas asociadas a esta solicitud
$sqlPart = "SELECT
p.id_partida, p.id_solicitud, p.descripcion, p.creado_en, p.cantidad_comercial,
p.cantidad_tarifa, p.valor_factura, p.peso_bruto, p.unidad_comercial_id, p.tasa_preferencial
FROM dbo.solicitud_importacion_partidas p
WHERE p.id_solicitud = ?
ORDER BY p.id_partida
";
$stmtPart = sqlsrv_query($conn, $sqlPart, [$id]);
if ($stmtPart === false) {
error_log("[update_status] Error al consultar partidas: " . print_r(sqlsrv_errors(), true));
echo json_encode(['success' => true, 'warning' => 'Status actualizado, fallo al leer partidas para pedimento']);
exit;
}
$partidas = [];
while ($row = sqlsrv_fetch_array($stmtPart, SQLSRV_FETCH_ASSOC)) {
if ($row['creado_en'] instanceof DateTime) {
$row['creado_en'] = $row['creado_en']->format('Y-m-d\TH:i:s');
}
$row['cantidad_comercial'] = floatval($row['cantidad_comercial']);
$row['cantidad_tarifa'] = floatval($row['cantidad_tarifa']);
$row['valor_factura'] = floatval($row['valor_factura']);
$row['peso_bruto'] = floatval($row['peso_bruto']);
$row['unidad_comercial_id'] = intval($row['unidad_comercial_id']);
$partidas[] = $row;
}
sqlsrv_free_stmt($stmtPart);
// 4.5) Construir el arreglo PHP con la misma estructura JSON que envías
$payload = [
"id_solicitud" => intval($solicitud['id_solicitud']),
"id_importador" => intval($solicitud['id_importador']),
"aduana" => strval($solicitud['aduana']),
"patente" => strval($solicitud['patente']),
"anexo22_apendice" => strval($solicitud['anexo22_apendice']),
"numero_factura" => strval($solicitud['numero_factura']),
"fecha_factura" => $fechaFactura,
"numero_pedimento" => "", // La API lo rellenará
"incoterm" => strval($solicitud['incoterm']),
"pais_proveedor" => strval($solicitud['pais_proveedor']),
"tipo_moneda" => strval($solicitud['tipo_moneda']),
"valor_factura" => floatval($solicitud['valor_factura']),
"vinculacion" => intval($solicitud['vinculacion']),
"transportista_id" => intval($solicitud['transportista_id']),
"created_at" => $createdAt,
"updated_at" => $updatedAt,
"transporte_id" => null,
"chofer_id" => intval($solicitud['chofer_id']),
"foto_solicitud_url"=> strval($solicitud['foto_solicitud_url']),
"proveedor_clave" => strval($solicitud['proveedor_clave']),
"partidas" => $partidas
];
$jsonPayload = json_encode($payload);
// 3.6) Preparar cURL para hacer POST a /pedimentos/crearPedimento
$apiBase = rtrim($_ENV['API_URL'] ?? '', '/');
$urlPed = $apiBase . '/pedimentos/crearPedimento';
$ch = curl_init($urlPed);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
"Authorization: $token",
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => $jsonPayload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$respPed = curl_exec($ch);
$httpPed = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
// 4.7) Verificar respuesta del endpoint pedimentos
if ($httpPed === 201 || $httpPed === 200) {
$decoded = json_decode($respPed, true);
// Si existe PEDIMENTO dentro de la clave 'pedimento'
if (isset($decoded['pedimento']['PEDIMENTO'])) {
$numeroPedimento = $decoded['pedimento']['PEDIMENTO'];
// 3.8) Actualizar localmente el campo numero_pedimento
$sqlUpdPed = "UPDATE dbo.solicitud_importacion_factura SET numero_pedimento = ? WHERE id_solicitud = ? AND id_importador = ?";
$stmtUpdPed = sqlsrv_query($conn, $sqlUpdPed, [$numeroPedimento, $id, $_SESSION['usuario_id']]);
if ($stmtUpdPed === false) {
error_log("[update_status] Error al actualizar numero_pedimento en BD: " . print_r(sqlsrv_errors(), true));
}
}
error_log("[update_status] Pedimento creado con éxito para solicitud $id → HTTP $httpPed$respPed");
echo json_encode([
'success' => true,
'pedimento' => $decoded['pedimento'],
'numeroPedimentoLocal' => $numeroPedimento
]);
exit;
} else {
error_log("[update_status] Error al crear pedimento (HTTP $httpPed): $respPed · cURL error: $curlErr");
echo json_encode([
'success' => true,
'warning' => "Status actualizado, pero fallo al crear pedimento (HTTP $respPed)"
]);
exit;
}
}
// 5) Si el status no es 2, devolvemos normal
echo json_encode(['success' => true]);
exit;
}
/** Envía notificación de cambio de estado de solicitud de importación **/
function enviarNotificacionCambioStatus($email, $nombreCompleto, $datosSolicitud, $esCorreoExtra = false)
{
// Validar datos de entrada
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
error_log("❌ Email inválido para notificación: $email");
return false;
}
if (empty($datosSolicitud) || !isset($datosSolicitud['nuevo_status'])) {
error_log("❌ Datos de solicitud incompletos para notificación");
return false;
}
$mail = new PHPMailer(true);
try {
// Configuración SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'] ?? '';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->CharSet = 'UTF-8';
// Configuración del mensaje
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($email);
$mail->isHTML(true);
// Obtener descripción y emoji del status
$statusInfo = obtenerInfoStatus($datosSolicitud['nuevo_status']);
// Personalizar subject si es correo adicional
$subjectPrefix = $esCorreoExtra ? '[COPIA] ' : '';
$mail->Subject = $subjectPrefix . '📊 Cambio de Estado en Solicitud de Importación';
// Preparar datos para el template
$datosTemplate = prepararDatosTemplate($datosSolicitud, $nombreCompleto, $statusInfo, $esCorreoExtra);
// Generar el HTML del email
$mail->Body = generarHtmlNotificacion($datosTemplate);
// Enviar el email
$envioExitoso = $mail->send();
if ($envioExitoso) {
$tipoCorreo = $esCorreoExtra ? 'correo adicional' : 'correo principal';
error_log("✅ Notificación de cambio de status enviada al $tipoCorreo: $email para solicitud ID: {$datosSolicitud['id_solicitud']}");
return true;
}
return false;
} catch (Exception $e) {
$tipoCorreo = $esCorreoExtra ? 'correo adicional' : 'correo principal';
error_log("❌ Error al enviar notificación de cambio de status al $tipoCorreo ($email): {$mail->ErrorInfo} | Exception: {$e->getMessage()}");
return false;
}
}
/** Obtiene información del status (descripción y emoji) **/
function obtenerInfoStatus($status) {
$statusMap = [
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']
];
return $statusMap[$status] ?? [
'emoji' => '📝',
'descripcion' => 'Estado actualizado',
'color' => '#6c757d'
];
}
/** Prepara los datos para el template del email **/
function prepararDatosTemplate($datosSolicitud, $nombreCompleto, $statusInfo, $esCorreoExtra)
{
return [
'nombreCompleto' => htmlspecialchars($nombreCompleto ?? 'Usuario'),
'idSolicitud' => intval($datosSolicitud['id_solicitud'] ?? 0),
'numeroFactura' => htmlspecialchars($datosSolicitud['numero_factura'] ?? 'N/A'),
'fechaFactura' => htmlspecialchars($datosSolicitud['fecha_factura'] ?? 'N/A'),
'valorFactura' => number_format(floatval($datosSolicitud['valor_factura'] ?? 0), 2),
'tipoMoneda' => htmlspecialchars($datosSolicitud['tipo_moneda'] ?? 'USD'),
'statusEmoji' => $statusInfo['emoji'],
'statusDescripcion' => $statusInfo['descripcion'],
'statusColor' => $statusInfo['color'],
'esCorreoExtra' => $esCorreoExtra,
'fechaActual' => date('Y-m-d H:i:s'),
'anioActual' => date('Y')
];
}
/** Genera el HTML para la notificación **/
function generarHtmlNotificacion($datos)
{
$tipoNotificacion = $datos['esCorreoExtra'] ?
'<div style="background: #fff3cd; padding: 10px; border-radius: 5px; margin-bottom: 15px; border-left: 4px solid #ffc107;">
<small><strong>📧 Copia enviada a correo adicional</strong></small>
</div>' : '';
return "
<div style='font-family: \"Segoe UI\", Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f6f9; padding: 30px; margin: 0;'>
<div style='max-width: 600px; margin: auto; background: #ffffff; border: 1px solid #dee2e6; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);'>
<!-- Header -->
<div style='background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 25px; text-align: center;'>
<h1 style='color: white; margin: 0; font-size: 24px; font-weight: 600;'>
{$datos['statusEmoji']} Estado Actualizado
</h1>
</div>
<!-- Content -->
<div style='padding: 30px 25px;'>
$tipoNotificacion
<p style='font-size: 16px; line-height: 1.5; margin-bottom: 20px;'>
Hola <strong>{$datos['nombreCompleto']}</strong>,
</p>
<p style='font-size: 16px; line-height: 1.5; margin-bottom: 25px;'>
El estado de tu solicitud de importación ha sido actualizado:
</p>
<!-- Status Card -->
<div style='background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px; padding: 20px; margin: 20px 0;'>
<table style='width: 100%; border-collapse: collapse; font-size: 14px;'>
<tr>
<td style='padding: 8px 0; font-weight: 600; color: #495057; width: 40%;'>ID Solicitud:</td>
<td style='padding: 8px 0; color: #212529;'>{$datos['idSolicitud']}</td>
</tr>
<tr>
<td style='padding: 8px 0; font-weight: 600; color: #495057;'>Número de Factura:</td>
<td style='padding: 8px 0; color: #212529;'>{$datos['numeroFactura']}</td>
</tr>
<tr>
<td style='padding: 8px 0; font-weight: 600; color: #495057;'>Fecha de Factura:</td>
<td style='padding: 8px 0; color: #212529;'>{$datos['fechaFactura']}</td>
</tr>
<tr style='background: rgba(40, 167, 69, 0.1);'>
<td style='padding: 12px 8px; font-weight: 700; color: #495057;'>
{$datos['statusEmoji']} Nuevo Estado:
</td>
<td style='padding: 12px 8px; font-weight: 700; color: {$datos['statusColor']};'>
{$datos['statusDescripcion']}
</td>
</tr>
<tr>
<td style='padding: 8px 0; font-weight: 600; color: #495057;'>Valor:</td>
<td style='padding: 8px 0; color: #212529; font-weight: 600;'>
{$datos['valorFactura']} {$datos['tipoMoneda']}
</td>
</tr>
</table>
</div>
<p style='font-size: 16px; line-height: 1.5; margin-top: 25px;'>
Puedes consultar todos los detalles y el seguimiento completo accediendo a tu panel de control.
</p>
<!-- Info Footer -->
<div style='margin-top: 30px; padding-top: 20px; border-top: 1px solid #e9ecef;'>
<p style='color: #6c757d; font-size: 13px; line-height: 1.4; margin: 0;'>
<strong>📧 Notificación automática</strong><br>
Este correo se envía automáticamente cuando se actualiza el estado de tu solicitud.
<br><small>Fecha de envío: {$datos['fechaActual']}</small>
</p>
</div>
</div>
<!-- Footer -->
<div style='background: #e9ecef; text-align: center; padding: 15px; font-size: 13px; color: #6c757d;'>
<strong>&copy; {$datos['anioActual']} SIIH</strong> · Desarrollado por
<span style='color: #007bff; font-weight: 600;'>AduanaSoft</span>
<br>
<small style='color: #adb5bd;'>Sistema Integral para Importadores de Hidrocarburos</small>
</div>
</div>
</div>";
}
// Función para generar el PDF
function pdf() {
// Verificar que se recibió el ID
if (!isset($_GET['id'])) {
http_response_code(400);
echo "ID de solicitud requerido";
return;
}
$id_solicitud = (int)$_GET['id'];
try {
// 1. Conexión
$conn = getConnection();
// 2. Obtener datos de la solicitud principal
$sql = "SELECT
s.*, i.nombre as importador_nombre, i.rfc as importador_rfc, i.calle, i.num_exterior,
i.num_interior, i.ciudad, i.colonia, i.codigo_postal, i.estado, i.telefono, i.correo,
t.nombre as transportista_nombre,
c.nombre as chofer_nombre
FROM solicitud_importacion_factura s
LEFT JOIN informacion_general i
ON s.id_importador = i.id_usuario
LEFT JOIN transportistas t
ON s.transportista_id = t.id_transportista
LEFT JOIN choferes c
ON s.chofer_id = c.id_chofer
WHERE s.id_solicitud = ?
";
$params = array($id_solicitud);
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
throw new Exception("Error en la consulta: " . print_r(sqlsrv_errors(), true));
}
$solicitud = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmt);
if (!$solicitud) {
http_response_code(404);
echo "Solicitud no encontrada";
return;
}
// 2.5. Obtener información del proveedor desde API
$proveedor_info = null;
if (!empty($solicitud['proveedor_clave'])) {
$proveedor_info = obtenerProveedorPorClave($solicitud['proveedor_clave']);
}
// 3. Obtener partidas de la solicitud
$sql = "SELECT
p.*, u.descripcion as unidad_descripcion
FROM solicitud_importacion_partidas p
LEFT JOIN unidades_medida_apendice7 u
ON p.unidad_comercial_id = u.id
WHERE p.id_solicitud = ?
ORDER BY p.id_partida
";
$params = array($id_solicitud);
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
throw new Exception("Error en la consulta de partidas: " . print_r(sqlsrv_errors(), true));
}
$partidas = array();
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$partidas[] = $row;
}
sqlsrv_free_stmt($stmt);
// 4. Configuración del sistema
$config = "SELECT * FROM configuracion_sistema";
$stmt = sqlsrv_query($conn, $config);
if ($stmt === false) {
throw new Exception("Error en la consulta: " . print_r(sqlsrv_errors(), true));
}
$configuracion = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
// 5. Generar HTML del PDF
$html = generarHTMLPDF($solicitud, $partidas, $configuracion, $proveedor_info);
// 6. Generar PDF usando DomPDF con Composer
require_once __DIR__ . '/../../vendor/autoload.php'; // Ajusta ruta si es necesario
$options = new Options();
$options->set('defaultFont', 'Arial');
$options->set('isRemoteEnabled', true); // Permite cargar imágenes remotas si es necesario
$options->set('isHtml5ParserEnabled', true); // Habilita el parser HTML5
$options->set('isPhpEnabled', true); // Habilita PHP dentro del HTML si es necesario
$options->set('chroot', $_SERVER['DOCUMENT_ROOT']); // Asegura que las rutas relativas funcionen correctamente
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
// Enviar el PDF al navegador
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="solicitud_importacion_' . $id_solicitud . '.pdf"');
echo $dompdf->output();
} catch (Exception $e) {
error_log("Error generando PDF: " . $e->getMessage());
http_response_code(500);
echo "Error interno del servidor: " . $e->getMessage();
}
}
// NUEVA FUNCIÓN: Obtener información del proveedor por clave
function obtenerProveedorPorClave($clave) {
// Obtener token de la API
$token = getApiToken();
if (!$token) {
error_log('[obtenerProveedorPorClave] Sin token válido');
return null;
}
// Construir URL de la API
$apiBase = rtrim($_ENV['API_URL'] ?? '', '/');
$url = $apiBase . '/proveedores';
// Ejecutar cURL para obtener todos los proveedores
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: $token", 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
error_log("[obtenerProveedorPorClave] Error HTTP $status al obtener proveedores");
return null;
}
$proveedores = json_decode($resp, true);
if (!is_array($proveedores)) {
error_log('[obtenerProveedorPorClave] Respuesta de API no es un array válido');
return null;
}
// Buscar el proveedor por clave
foreach ($proveedores as $proveedor) {
// Probar tanto 'Clave' como 'CLAVE' por si acaso
$proveedor_clave = $proveedor['CLAVE'] ?? $proveedor['Clave'] ?? null;
if ($proveedor_clave === $clave) {
return $proveedor;
}
}
error_log("[obtenerProveedorPorClave] Proveedor con clave '$clave' no encontrado");
return null;
}
function generarHTMLPDF($solicitud, $partidas, $configuracion, $proveedor_info = null) {
// Formatear fecha
$fecha_expedicion = $solicitud['fecha_factura']->format('d/m/Y');
$fecha_vencimiento = $solicitud['fecha_factura']->modify('+30 days')->format('d/m/Y');
// Construir dirección del importador
$direccion_completa = trim(
($solicitud['calle'] ?? '') . ' ' .
($solicitud['num_exterior'] ?? '') . ' ' .
($solicitud['num_interior'] ? 'Int. ' . $solicitud['num_interior'] : '') . ', ' .
($solicitud['colonia'] ?? '') . ', ' .
($solicitud['ciudad'] ?? '') . ', ' .
($solicitud['estado'] ?? '') . ' ' .
($solicitud['codigo_postal'] ?? '')
);
// Construir información del proveedor
$proveedor_nombre = 'Proveedor no disponible';
$proveedor_rfc = 'RFC no disponible';
$proveedor_direccion = 'Dirección no disponible';
$proveedor_telefono = 'Teléfono no disponible';
// Construir información del proveedor
if ($proveedor_info) {
// Usar nombres de campos en mayúsculas según la estructura de la tabla
$proveedor_nombre = $proveedor_info['Nombre'] ?? 'Nombre no disponible';
$proveedor_rfc = $proveedor_info['IdentFiscal'] ??$proveedor_info['RFC'] ?? 'RFC no disponible';
$proveedor_telefono = trim($proveedor_info['Telefono'] ?? 'Teléfono no disponible');
// Construir dirección del proveedor usando los campos correctos
$direccion_partes = array_filter([
$proveedor_info['Calles'] ?? '',
$proveedor_info['Colonia'] ?? '',
$proveedor_info['Municipio'] ?? '',
$proveedor_info['Ciudad'] ?? '',
($proveedor_info['CodigoPostal'] ?? '') ? 'C.P. ' . $proveedor_info['CodigoPostal'] : '',
$proveedor_info['EntidadFederativa'] ?? '',
$proveedor_info['Pais'] ?? ''
]);
if (!empty($direccion_partes)) {
$proveedor_direccion = implode(', ', $direccion_partes);
}
}
// Calcular el total sumando todas las partidas
$total = 0;
foreach ($partidas as $partida) {
$total += (float)($partida['valor_factura'] ?? 0);
}
// Función para convertir número a texto
function numeroATexto($numero) {
$unidades = ['', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve'];
$decenas = ['', '', 'veinte', 'treinta', 'cuarenta', 'cincuenta', 'sesenta', 'setenta', 'ochenta', 'noventa'];
$especiales = ['diez', 'once', 'doce', 'trece', 'catorce', 'quince', 'dieciséis', 'diecisiete', 'dieciocho', 'diecinueve'];
$centenas = ['', 'ciento', 'doscientos', 'trescientos', 'cuatrocientos', 'quinientos', 'seiscientos', 'setecientos', 'ochocientos', 'novecientos'];
if ($numero == 0) return 'cero';
if ($numero == 100) return 'cien';
if ($numero == 1000) return 'mil';
if ($numero == 1000000) return 'un millón';
$resultado = '';
// Millones
if ($numero >= 1000000) {
$millones = intval($numero / 1000000);
if ($millones == 1) {
$resultado .= 'un millón ';
} else {
$resultado .= numeroATexto($millones) . ' millones ';
}
$numero %= 1000000;
}
// Miles
if ($numero >= 1000) {
$miles = intval($numero / 1000);
if ($miles == 1) {
$resultado .= 'mil ';
} else {
$resultado .= numeroATexto($miles) . ' mil ';
}
$numero %= 1000;
}
// Centenas
if ($numero >= 100) {
$resultado .= $centenas[intval($numero / 100)] . ' ';
$numero %= 100;
}
// Decenas y unidades
if ($numero >= 20) {
$resultado .= $decenas[intval($numero / 10)];
if ($numero % 10 != 0) {
$resultado .= ' y ' . $unidades[$numero % 10];
}
} elseif ($numero >= 10) {
$resultado .= $especiales[$numero - 10];
} elseif ($numero > 0) {
$resultado .= $unidades[$numero];
}
return trim($resultado);
}
// Obtener la moneda de la solicitud o usar MXN por defecto
$moneda_codigo = $solicitud['tipo_moneda'] ?? 'MXN';
// Configuración de monedas
$monedas_config = [
'MXN' => ['nombre' => 'PESOS', 'sufijo' => 'M.N.', 'centavos' => 'CENTAVOS'],
'USD' => ['nombre' => 'DÓLARES', 'sufijo' => 'USD', 'centavos' => 'CENTAVOS'],
'EUR' => ['nombre' => 'EUROS', 'sufijo' => 'EUR', 'centavos' => 'CÉNTIMOS'],
'CNY' => ['nombre' => 'YUANES', 'sufijo' => 'CNY', 'centavos' => 'JIAO'],
'GBP' => ['nombre' => 'LIBRAS', 'sufijo' => 'GBP', 'centavos' => 'PENIQUES'],
'JPY' => ['nombre' => 'YENES', 'sufijo' => 'JPY', 'centavos' => 'SEN']
];
$config_moneda = $monedas_config[$moneda_codigo] ?? $monedas_config['MXN'];
// Convertir total a texto
$partes = explode('.', number_format($total, 2, '.', ''));
$enteros = (int)$partes[0];
$decimales = (int)$partes[1];
$total_texto = strtoupper(numeroATexto($enteros)) . ' ' . $config_moneda['nombre'];
// Para JPY no se usan decimales tradicionalmente
if ($moneda_codigo === 'JPY') {
$total_texto .= ' ' . $config_moneda['sufijo'];
} else {
if ($decimales > 0) {
$total_texto .= ' CON ' . str_pad($decimales, 2, '0', STR_PAD_LEFT) . '/100 ' . $config_moneda['sufijo'];
} else {
$total_texto .= ' 00/100 ' . $config_moneda['sufijo'];
}
}
$html = '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Solicitud de Importación</title>
<style>
body { font-family: Arial, sans-serif; font-size: 9px; margin: 0; padding: 15px; line-height: 1.2; }
/** Encabezado **/
.header { padding-bottom: 50px; }
.logo-section { width: 20%; text-align: left; }
.logo { max-width: 125px; height: auto; vertical-align: top; }
.company-info { width: 60%; text-align: center; vertical-align: top; font-size: 12px; }
.company-name { font-weight: bold; font-size: 25px; margin-bottom: 3px; }
.invoice-info { width: 20%; text-align: right; vertical-align: top; font-size: 12px; }
/** Sección de Información **/
.info-section { border: 1px solid black; border-collapse: collapse; width: 100%; table-layout: fixed; }
.clave-section { border: 0.5px solid black; border-collapse: collapse; width: 100%; table-layout: fixed; padding-bottom: 15px; }
/** Información del Proveedor **/
.proveedor-info { width: 100%; }
.provedor-info table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.p-field { width: 100px; background-color: #d0d0d0; font-weight: bold; text-align: center; font-size: 12px; }
.field { border-bottom: 0.5px solid #000; padding: 5px; font-size: 12px; }
/** Fechas **/
.dates-info { width: 25%; border: 1px solid #000; }
.dates-info table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.d-field { background-color: #d0d0d0; font-weight: bold; text-align: center; font-size: 12px; }
.date { font-weight: bold; text-align: center; font-size: 12px; padding: 7.5px; }
/** Partidas **/
.products-table { border-collapse: collapse; border: 0.5px solid #000; }
.products-table td { border: 0.5px solid #000; padding: 10px; text-align: center; font-size: 10px; }
.products-table th { border: 0.5px solid #000; padding: 5px; background-color: #d0d0d0; font-weight: bold; text-align: center; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-bold { font-weight: bold; }
/** Total **/
.totals-section { float: right; width: 250px; }
.total-row { display: flex; justify-content: space-between; margin-top: 25px; font-size: 12px; }
/** Nota inferior **/
.footer-info { }
.footer-note { font-size: 10px; background-color: #d0d0d0; padding: 5px; border: 0.5px solid #000; }
</style>
</head>
<body>
<!-- ENCABEZADO -->
<table class="header" cellspacing="0" cellpadding="0" width="100%">
<tr>
<td class="logo-section">
<img src="' . htmlspecialchars($configuracion['logo_url'] ?? 'assets/img/logo_siih.png') . '" alt="Logo" class="logo"><br>
</td>
<td class="company-info">
<div class="company-name">' . htmlspecialchars($solicitud['importador_nombre']) . '</div>
<div>' . htmlspecialchars($direccion_completa ?: 'Dirección no disponible') . '</div>
<div>RFC: ' . htmlspecialchars($solicitud['importador_rfc'] ?? 'No disponible') . '</div>
<div>Tel: ' . htmlspecialchars($solicitud['telefono'] ?? 'No disponible') . '</div>
<div>Email: ' . htmlspecialchars($solicitud['correo'] ?? 'No disponible') . '</div>
</td>
<td class="invoice-info">
<div><strong>' . htmlspecialchars($configuracion['nombre_plataforma'] ?? 'Sistema Integral para Importadores de Hidrocarburos') . '</strong></div><br>
<div class="invoice-title">Solicitud de Importación</div>
<div><strong>No. ' . htmlspecialchars($solicitud['id_solicitud']) . '</strong></div>
</td>
</tr>
</table>
<!-- SECCIÓN DE INFORMACIÓN DEL PROVEEDOR Y FECHAS -->
<table class="info-section" cellspacing="0" cellpadding="0">
<tr>
<!-- PROVEEDOR -->
<td>
<table class="proveedor-info" cellspacing="0" cellpadding="0">
<tr>
<td class="p-field">RAZÓN SOCIAL:</td>
<td class="field">' . htmlspecialchars($proveedor_nombre) . '</td>
</tr>
</table>
<table class="proveedor-info" cellspacing="0" cellpadding="0">
<tr>
<td class="p-field" style="height: 45px;">DIRECCIÓN:</td>
<td class="field">' . htmlspecialchars($proveedor_direccion) . '</td>
</tr>
</table>
<table class="proveedor-info" cellspacing="0" cellpadding="0">
<tr>
<td class="p-field">RFC:</td>
<td class="field" style="border-bottom: none;">' . htmlspecialchars($proveedor_rfc) . '</td>
</tr>
</table>
</td>
<!-- FECHAS -->
<td class="dates-info">
<table cellspacing="0" cellpadding="2">
<tr><td class="d-field" style="border-left: 0.5px solid black;">FECHA DE EXPEDICIÓN</td></tr>
<tr><td class="date" style="border-bottom: 0.5px solid black;">' . $fecha_expedicion . '</td></tr>
<tr><td class="d-field" style="border-left: 0.5px solid black;">FECHA DE VENCIMIENTO</td></tr>
<tr><td class="date">' . $fecha_vencimiento . '</td></tr>
</table>
</td>
</tr>
</table>
<table class="clave-section" cellspacing="0" cellpadding="0">
<tr>
<td class="p-field">CLAVE:</td>
<td class="field" style="border-bottom: none;">' . htmlspecialchars($solicitud['proveedor_clave'] ?? 'No disponible') . '</td>
<td class="p-field">TELÉFONO:</td>
<td class="field" style="border-bottom: none;">' . htmlspecialchars($proveedor_telefono) . '</td>
</tr>
</table>
<!-- TABLA DE PRODUCTOS/PARTIDAS -->
<table class="products-table" cellspacing="0" cellpadding="0" width="100%">
<thead>
<tr>
<th>Producto</th>
<th>Unidad de Medida</th>
<th>Precio Unitario</th>
<th>Cantidad</th>
<th>Total</th>
</tr>
</thead>
<tbody>';
// Agregar partidas
foreach ($partidas as $partida) {
$precio_unitario = (float)($partida['precio_unitario'] ?? 0);
$cantidad = (float)($partida['cantidad_comercial'] ?? 0);
$valor_partida = (float)($partida['valor_factura'] ?? 0);
$html .= '
<tr>
<td>' . htmlspecialchars($partida['descripcion']) . '</td>
<td>' . htmlspecialchars($partida['unidad_descripcion'] ?? 'Unidad de servicio (E48)') . '</td>
<td>' . $moneda_codigo . ' ' . number_format($precio_unitario > 0 ? $precio_unitario : $valor_partida, 2) . '</td>
<td>' . number_format($cantidad > 0 ? $cantidad : 1, 0) . '</td>
<td>' . $moneda_codigo . ' ' . number_format($valor_partida, 2) . '</td>
</tr>';
}
$html .= '
</tbody>
</table>
<!-- NOTA INFERIOR -->
<div class="footer-info">
<div class="footer-note">' . $total_texto . '</div>
</div>
<!-- TOTALES -->
<div class="totals-section">
<div class="total-row text-right">
<span><strong>Total:</strong></span>
<span><strong>' . $moneda_codigo . ' ' . number_format($total, 2) . '</strong></span>
</div>
</div>
</body>
</html>';
return $html;
}