Tul xxx Tul
User / IP
:
216.73.216.159
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
emprendo.com.co
/
public_html2
/
cuentame
/
controllers
/
Viewing: AdminController.php
<?php require_once '../cuentame/core/Controller.php'; require_once __DIR__ . '/../core/Helper.php'; class AdminController extends Controller { public function __construct() { session_start(); // Permitir acceso público solo a getProductsByLinea $publicMethods = ['getProductsByLinea']; $currentMethod = $_GET['url'] ?? ''; $currentMethod = explode('/', $currentMethod)[1] ?? ''; if (!in_array($currentMethod, $publicMethods)) { if (!isset($_SESSION['user_id'])) { header('Location: ' . Helper::appUrl('login')); exit; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $role = $userModel->getRole($_SESSION['user_id']); if ($role !== 'admin') { header('Location: ' . Helper::appUrl('login')); exit; } } } public function projectPayment() { header('Content-Type: application/json'); require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/PaidProject.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $projectModel = new Project(); $paidModel = new PaidProject(); $actionTotalsModel = new ProjectAction(); $normalize = function ($row) { if (!$row) return null; return [ 'id' => (int)($row['id'] ?? 0), 'project_id' => (int)($row['project_id'] ?? 0), 'amount' => (int)($row['amount'] ?? 0), 'observations' => $row['observations'] ?? null, 'created_at' => $row['created_at'] ?? null, 'updated_at' => $row['updated_at'] ?? null, ]; }; if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (isset($_GET['payment_id'])) { $paymentId = (int)$_GET['payment_id']; if ($paymentId <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de abono inválido']); return; } $payment = $paidModel->getById($paymentId); if (!$payment) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Abono no encontrado']); return; } echo json_encode([ 'success' => true, 'data' => [ 'project_id' => (int)($payment['project_id'] ?? 0), 'payment' => $normalize($payment) ] ]); return; } if (!isset($_GET['project_id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de proyecto requerido']); return; } $projectId = (int)$_GET['project_id']; $project = $projectModel->getById($projectId); if (!$project) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Proyecto no encontrado']); return; } $totals = $actionTotalsModel->getTotalsByProjectId($projectId); $summary = $paidModel->getSummaryByProjectId($projectId); $entries = $paidModel->getAllByProjectId($projectId); $normalizedEntries = array_map($normalize, $entries ?: []); echo json_encode([ 'success' => true, 'data' => [ 'project_id' => $projectId, 'total_amount' => (int)($summary['total_amount'] ?? ($project['paid_amount'] ?? 0)), 'entries_count' => (int)($summary['entries_count'] ?? count($normalizedEntries)), 'latest' => $normalize($summary['latest'] ?? null), 'entries' => $normalizedEntries, 'budget' => (int)($totals['total_price'] ?? 0) ] ]); return; } $op = $_POST['op'] ?? ''; if ($op === 'update_field') { $paymentId = isset($_POST['payment_id']) ? (int)$_POST['payment_id'] : 0; if ($paymentId <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de abono inválido']); return; } $entry = $paidModel->getById($paymentId); if (!$entry) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Abono no encontrado']); return; } $projectId = (int)($entry['project_id'] ?? 0); $field = $_POST['field'] ?? ''; $value = $_POST['value'] ?? ''; $amount = null; $observations = null; if ($field === 'amount') { $digits = preg_replace('/[^0-9]/', '', (string)$value); $amount = (int)max(0, $digits !== '' ? (int)$digits : 0); } elseif ($field === 'observations') { $observations = trim((string)$value); } else { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Campo no soportado']); return; } if (!$paidModel->updateFields($paymentId, $amount, $observations)) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'No se pudo actualizar el abono']); return; } $summary = $paidModel->getSummaryByProjectId($projectId); $entries = $paidModel->getAllByProjectId($projectId); $totals = $actionTotalsModel->getTotalsByProjectId($projectId); $normalizedEntries = array_map($normalize, $entries ?: []); $totalAmount = (int)($summary['total_amount'] ?? 0); $projectModel->updatePaidAmount($projectId, $totalAmount); echo json_encode([ 'success' => true, 'data' => [ 'project_id' => $projectId, 'total_amount' => $totalAmount, 'entries_count' => (int)($summary['entries_count'] ?? count($normalizedEntries)), 'latest' => $normalize($summary['latest'] ?? null), 'entries' => $normalizedEntries, 'budget' => (int)($totals['total_price'] ?? 0) ] ]); return; } if ($op === 'delete') { $paymentId = isset($_POST['payment_id']) ? (int)$_POST['payment_id'] : 0; if ($paymentId <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de abono inválido']); return; } $entry = $paidModel->getById($paymentId); if (!$entry) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Abono no encontrado']); return; } $projectId = (int)($entry['project_id'] ?? 0); if (!$paidModel->deleteById($paymentId)) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'No se pudo eliminar el abono']); return; } $summary = $paidModel->getSummaryByProjectId($projectId); $entries = $paidModel->getAllByProjectId($projectId); $totals = $actionTotalsModel->getTotalsByProjectId($projectId); $normalizedEntries = array_map($normalize, $entries ?: []); $totalAmount = (int)($summary['total_amount'] ?? 0); $projectModel->updatePaidAmount($projectId, $totalAmount); echo json_encode([ 'success' => true, 'data' => [ 'project_id' => $projectId, 'total_amount' => $totalAmount, 'entries_count' => (int)($summary['entries_count'] ?? count($normalizedEntries)), 'latest' => $normalize($summary['latest'] ?? null), 'entries' => $normalizedEntries, 'budget' => (int)($totals['total_price'] ?? 0) ] ]); return; } if (!isset($_POST['project_id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de proyecto requerido']); return; } $projectId = (int)$_POST['project_id']; if ($projectId <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de proyecto inválido']); return; } $project = $projectModel->getById($projectId); if (!$project) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Proyecto no encontrado']); return; } $amount = isset($_POST['amount']) ? (int)max(0, $_POST['amount']) : 0; $observations = isset($_POST['observations']) ? trim($_POST['observations']) : null; if (!$paidModel->upsert($projectId, $amount, $observations)) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'No se pudo actualizar el pago']); return; } $summary = $paidModel->getSummaryByProjectId($projectId); $entries = $paidModel->getAllByProjectId($projectId); $totals = $actionTotalsModel->getTotalsByProjectId($projectId); $normalizedEntries = array_map($normalize, $entries ?: []); $latest = $normalize($summary['latest'] ?? null); $totalAmount = (int)($summary['total_amount'] ?? 0); $projectModel->updatePaidAmount($projectId, $totalAmount); echo json_encode([ 'success' => true, 'data' => [ 'project_id' => $projectId, 'total_amount' => $totalAmount, 'latest' => $latest, 'entries' => $normalizedEntries, 'entries_count' => (int)($summary['entries_count'] ?? count($normalizedEntries)), 'budget' => (int)($totals['total_price'] ?? 0) ] ]); } // Método helper para obtener datos del usuario private function getUserData() { require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $user = $userModel->findById($_SESSION['user_id']); return [ 'user_name' => $_SESSION['user_name'] ?? ($user['name'] ?? 'Admin'), 'user_email' => $user['email'] ?? '', 'profile_image' => $user['profile_image'] ?? Helper::asset('assets/img/user-default.png') ]; } public function index() { // Redirigir al dashboard por defecto header('Location: ' . Helper::appUrl('admin/dashboard')); exit; } public function dashboard() { $this->bienvenida(); } public function usuarios() { require_once __DIR__ . '/../models/User.php'; require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/Payment.php'; require_once __DIR__ . '/../models/Activity.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $userModel = new User(); $projectModel = new Project(); $paymentModel = new Payment(); $activityModel = new Activity(); $paActionModel = new ProjectAction(); $data = $this->getUserData(); $data['users'] = $userModel->getAll(); $data['title'] = 'Gestión de Usuarios'; // Enriquecer cada usuario con KPIs compactos para mini-dashboard foreach ($data['users'] as &$u) { try { $uid = (int)($u['id'] ?? 0); // Proyectos por usuario $userProjects = $projectModel->getByUser($uid); $activeStatuses = ['En Progreso']; // solo proyectos en progreso $pendingStatuses = ['Pendiente']; $completedStatuses = ['Logrado']; // solo proyectos logrados $actives = 0; $pending = 0; $completed = 0; foreach ($userProjects as $prj) { $st = $prj['status'] ?? ''; if (in_array($st, $activeStatuses, true)) { $actives++; } if (in_array($st, $pendingStatuses, true)) { $pending++; } if (in_array($st, $completedStatuses, true)) { $completed++; } } // Proyectos pendientes (En Espera) $upcoming = (int)$pending; // Presupuestado: suma del precio de acciones en Pendiente o En Progreso $budgetedAmount = (int)$paActionModel->getSumPrecioPendienteEnProgresoByUser($uid) ?: 0; // Saldo disponible (pagos Recibido - abonado a proyectos) $received = (int)$paymentModel->getTotalByUser($uid) ?: 0; $paidToProj = (int)$paymentModel->getTotalPaidToProjects($uid) ?: 0; $available = (int)$received - (int)$paidToProj; if ($available < 0) { $available = 0; } $u['kpi_active_projects'] = $actives; $u['kpi_completed_projects'] = $completed; $u['kpi_pending_projects'] = $upcoming; $u['kpi_upcoming_activities'] = $upcoming; // compatibilidad $u['kpi_budgeted_amount'] = (int)round($budgetedAmount); $u['kpi_available_balance'] = (int)round($available); $u['kpi_balance_received_sum'] = (int)$received; $u['kpi_balance_paid_sum'] = (int)$paidToProj; // Nuevo KPI: Pendiente por pagar = Suma precio acciones Logradas - Abonos a proyectos try { $sumLograda = (int)$paActionModel->getSumPrecioLogradaByUser($uid); $sumAbonos = (int)$paymentModel->getTotalPaidToProjects($uid); $dueToPay = $sumLograda - $sumAbonos; if ($dueToPay < 0) { $dueToPay = 0; } $u['kpi_due_to_pay'] = $dueToPay; $u['kpi_due_actions_sum'] = $sumLograda; $u['kpi_due_paid_sum'] = $sumAbonos; } catch (Throwable $__e) { $u['kpi_due_to_pay'] = 0; $u['kpi_due_actions_sum'] = 0; $u['kpi_due_paid_sum'] = 0; } } catch (Throwable $__) { // En caso de error silencioso, exponer ceros $u['kpi_active_projects'] = 0; $u['kpi_completed_projects'] = 0; $u['kpi_pending_projects'] = 0; $u['kpi_upcoming_activities'] = 0; $u['kpi_budgeted_amount'] = 0; $u['kpi_available_balance'] = 0; $u['kpi_balance_received_sum'] = 0; $u['kpi_balance_paid_sum'] = 0; $u['kpi_due_to_pay'] = 0; $u['kpi_due_actions_sum'] = 0; $u['kpi_due_paid_sum'] = 0; } } unset($u); // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['user_action'] ?? ''; switch ($action) { case 'create': // Validar campos requeridos if (empty($_POST['name']) || empty($_POST['email']) || empty($_POST['password'])) { $_SESSION['error'] = 'Todos los campos son requeridos para crear un usuario'; break; } // Manejar subida de imagen de perfil $profile_image = null; if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/../../uploads/profile_images/'; if (!is_dir($upload_dir)) { mkdir($upload_dir, 0777, true); } $file = $_FILES['profile_image']; $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = 'user_' . time() . '.' . $ext; $filepath = $upload_dir . $filename; if (move_uploaded_file($file['tmp_name'], $filepath)) { $profile_image = Helper::asset('uploads/profile_images/' . $filename); } } $result = $userModel->create( $_POST['name'], $_POST['email'], $_POST['password'], $_POST['telefono'] ?? null, $_POST['documento'] ?? null, $_POST['role'], $profile_image, $_POST['project_manager'] ?: null ); if ($result) { $_SESSION['success'] = 'Usuario creado exitosamente'; } else { $_SESSION['error'] = 'Error al crear el usuario. El email ya puede estar registrado.'; } break; case 'edit': // Validar campos requeridos if (empty($_POST['name']) || empty($_POST['email'])) { $_SESSION['error'] = 'Nombre y email son requeridos'; break; } // Manejar subida de imagen de perfil $profile_image = null; if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/../../uploads/profile_images/'; if (!is_dir($upload_dir)) { mkdir($upload_dir, 0777, true); } $file = $_FILES['profile_image']; $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = 'user_' . $_POST['user_id'] . '_' . time() . '.' . $ext; $filepath = $upload_dir . $filename; if (move_uploaded_file($file['tmp_name'], $filepath)) { $profile_image = Helper::asset('uploads/profile_images/' . $filename); } } else if (!empty($_POST['current_profile_image'])) { $profile_image = $_POST['current_profile_image']; } $result = $userModel->updateUser( $_POST['user_id'], $_POST['name'], $_POST['email'], $_POST['telefono'] ?? null, $_POST['documento'] ?? null, $_POST['role'], $_POST['password'] ?: null, $profile_image, $_POST['project_manager'] ?: null ); if ($result) { $_SESSION['success'] = 'Usuario actualizado exitosamente'; } else { $_SESSION['error'] = 'Error al actualizar el usuario. El email ya puede estar registrado por otro usuario.'; } break; case 'delete': $result = $userModel->delete($_POST['user_id']); if ($result) { $_SESSION['success'] = 'Usuario eliminado exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar el usuario'; } break; } header('Location: ' . Helper::appUrl('admin/usuarios')); exit; } $content = $this->renderViewToString('admin/usuarios', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function verAcuerdo() { $data = $this->getUserData(); $data['title'] = 'Ver Acuerdo del Usuario'; $userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0; if ($userId <= 0) { $_SESSION['error'] = 'ID de usuario inválido'; header('Location: ' . Helper::appUrl('admin/usuarios')); exit; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $user = $userModel->findById($userId); if (!$user) { $_SESSION['error'] = 'Usuario no encontrado'; header('Location: ' . Helper::appUrl('admin/usuarios')); exit; } $agreement = $userModel->getAgreementData($userId); if (empty($agreement['accepted_at'])) { $_SESSION['error'] = 'El usuario aún no ha aceptado el acuerdo'; header('Location: ' . Helper::appUrl('admin/usuarios')); exit; } $signatureSrc = null; if (!empty($agreement['signature_blob'])) { $signatureSrc = 'data:image/png;base64,' . base64_encode($agreement['signature_blob']); } elseif (!empty($agreement['signature_path'])) { $signatureSrc = $agreement['signature_path']; } $data['view_user'] = $user; $data['agreement'] = $agreement; $data['agreement_signature_src'] = $signatureSrc; $data['agreement_photo_src'] = $agreement['photo'] ?? null; $content = $this->renderViewToString('admin/ver_acuerdo', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function emprendimientos() { require_once __DIR__ . '/../models/User.php'; require_once __DIR__ . '/../models/Emprendimiento.php'; require_once __DIR__ . '/../models/Product.php'; $userModel = new User(); $emprendimientoModel = new Emprendimiento(); $productModel = new Product(); $data = $this->getUserData(); $data['users'] = $userModel->getAll(); $data['emprendimientos'] = $emprendimientoModel->getAll(); // Para el modal de proyectos embebido en esta vista $data['products'] = $productModel->getAll(); $data['admins_and_managers'] = $userModel->getAdminsAndManagers(); $data['title'] = 'Gestión de eMprendimientos'; // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_emprendimiento': $emprendimientoData = [ 'user_id' => $_POST['user_id'], 'nombre_comercial' => $_POST['nombre_comercial'], 'razon_social' => $_POST['razon_social'], 'status' => $_POST['status'], 'forma_juridica' => $_POST['forma_juridica'], 'documento' => $_POST['documento'], 'telefono' => $_POST['telefono'], 'telefono2' => $_POST['telefono2'], 'pais' => $_POST['pais'], 'localidad' => $_POST['localidad'], 'direccion' => $_POST['direccion'], 'pagina_web' => $_POST['pagina_web'], 'email' => $_POST['email'], 'sector_empresarial' => $_POST['sector_empresarial'], 'rubro_especifico' => $_POST['rubro_especifico'], 'fundacion' => $_POST['fundacion'], 'trayectoria' => $_POST['trayectoria'], 'tiempo_interrupcion' => $_POST['tiempo_interrupcion'] ?? null, 'motivo_interrupcion' => $_POST['motivo_interrupcion'] ?? null, 'descripcion_empresa' => $_POST['descripcion_empresa'], 'productos_servicios' => $_POST['productos_servicios'], 'numero_trabajadores' => $_POST['numero_trabajadores'], 'presencia_internacional' => $_POST['presencia_internacional'], 'comentarios' => $_POST['comentarios'] ]; $result = $emprendimientoModel->create($emprendimientoData); // Manejar subida de archivos if ($result) { $logo_path = $this->handleEmprendimientoUpload('logo'); $avatar_path = $this->handleEmprendimientoUpload('avatar'); if ($logo_path || $avatar_path) { $emprendimientoModel->updateFiles($result, $logo_path, $avatar_path); } $_SESSION['success'] = 'Emprendimiento creado exitosamente'; // Notificar al usuario por chat try { require_once __DIR__ . '/../models/Chat.php'; require_once __DIR__ . '/../models/User.php'; $chat = new Chat(); $adminId = (int)($_SESSION['user_id'] ?? 0); $clientId = (int)($_POST['user_id'] ?? 0); if ($adminId > 0 && $clientId > 0) { $convId = $chat->ensureConversation($adminId, $clientId); $empId = (int)$result; // id recién creado $empName = trim($_POST['nombre_comercial'] ?? 'tu eMprendimiento'); $link = Helper::appUrl('clientes/emprendimientos') . '#emp=' . $empId; $msg = "🎉 ¡Se creó un nuevo eMprendimiento para ti!\n[" . ($empName !== '' ? $empName : 'eMprendimiento') . "](" . $link . ")"; $chat->insertMessage($convId, $adminId, $msg, 'text', null); } } catch (Throwable $e) { /* noop */ } } else { $_SESSION['error'] = 'Error al crear el emprendimiento'; } break; case 'update_emprendimiento': $emprendimientoData = [ 'user_id' => $_POST['user_id'], 'nombre_comercial' => $_POST['nombre_comercial'], 'razon_social' => $_POST['razon_social'], 'status' => $_POST['status'], 'forma_juridica' => $_POST['forma_juridica'], 'documento' => $_POST['documento'], 'telefono' => $_POST['telefono'], 'telefono2' => $_POST['telefono2'], 'pais' => $_POST['pais'], 'localidad' => $_POST['localidad'], 'direccion' => $_POST['direccion'], 'pagina_web' => $_POST['pagina_web'], 'email' => $_POST['email'], 'sector_empresarial' => $_POST['sector_empresarial'], 'rubro_especifico' => $_POST['rubro_especifico'], 'fundacion' => $_POST['fundacion'], 'trayectoria' => $_POST['trayectoria'], 'tiempo_interrupcion' => $_POST['tiempo_interrupcion'] ?? null, 'motivo_interrupcion' => $_POST['motivo_interrupcion'] ?? null, 'descripcion_empresa' => $_POST['descripcion_empresa'], 'productos_servicios' => $_POST['productos_servicios'], 'numero_trabajadores' => $_POST['numero_trabajadores'], 'presencia_internacional' => $_POST['presencia_internacional'], 'comentarios' => $_POST['comentarios'], // Logo y avatar: si se subió uno nuevo, usarlo; si no, mantener el existente 'logo' => null, 'avatar' => null ]; $logo_path = $this->handleEmprendimientoUpload('logo'); $avatar_path = $this->handleEmprendimientoUpload('avatar'); $emprendimientoData['logo'] = $logo_path ?: ($_POST['existing_logo'] ?? null); $emprendimientoData['avatar'] = $avatar_path ?: ($_POST['existing_avatar'] ?? null); $result = $emprendimientoModel->update($_POST['emprendimiento_id'], $emprendimientoData); if ($result) { $_SESSION['success'] = 'Emprendimiento actualizado exitosamente'; // Notificar al usuario por chat try { require_once __DIR__ . '/../models/Chat.php'; $chat = new Chat(); $adminId = (int)($_SESSION['user_id'] ?? 0); $clientId = (int)($_POST['user_id'] ?? 0); if ($adminId > 0 && $clientId > 0) { $convId = $chat->ensureConversation($adminId, $clientId); $empId = (int)($_POST['emprendimiento_id'] ?? 0); $empName = trim($_POST['nombre_comercial'] ?? 'tu eMprendimiento'); $link = $empId > 0 ? (Helper::appUrl('clientes/emprendimientos') . '#emp=' . $empId) : Helper::appUrl('clientes/emprendimientos'); $msg = "✏️ Se actualizó tu eMprendimiento\n[" . ($empName !== '' ? $empName : 'eMprendimiento') . "](" . $link . ")"; $chat->insertMessage($convId, $adminId, $msg, 'text', null); } } catch (Throwable $e) { /* noop */ } } else { $_SESSION['error'] = 'Error al actualizar el emprendimiento'; } break; case 'delete_emprendimiento': $result = $emprendimientoModel->delete($_POST['emprendimiento_id']); if ($result) { $_SESSION['success'] = 'Emprendimiento eliminado exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar el emprendimiento'; } break; } header('Location: ' . Helper::appUrl('admin/emprendimientos')); exit; } $content = $this->renderViewToString('admin/emprendimientos', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } private function handleEmprendimientoUpload($field) { if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK || $_FILES[$field]['size'] === 0) { return null; } $file = $_FILES[$field]; $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $upload_dir = __DIR__ . '/../../uploads/emprendimientos/'; if (!is_dir($upload_dir)) { mkdir($upload_dir, 0777, true); } $filename = $field . '_' . time() . '.' . $ext; $filepath = $upload_dir . $filename; if (move_uploaded_file($file['tmp_name'], $filepath)) { return Helper::asset('uploads/emprendimientos/' . $filename); } return null; } public function viewEmprendimiento() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Emprendimiento.php'; require_once __DIR__ . '/../models/User.php'; $emprendimientoModel = new Emprendimiento(); $userModel = new User(); $emprendimiento = $emprendimientoModel->findById($_GET['id']); if ($emprendimiento) { // Obtener datos del cliente $cliente = $userModel->findById($emprendimiento['user_id']); $emprendimiento['cliente_nombre'] = $cliente ? $cliente['name'] : 'Desconocido'; $emprendimiento['cliente_email'] = $cliente ? $cliente['email'] : ''; echo json_encode(['success' => true, 'emprendimiento' => $emprendimiento]); } else { echo json_encode(['success' => false, 'message' => 'Emprendimiento no encontrado']); } } public function getEmprendimiento() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Emprendimiento.php'; $emprendimientoModel = new Emprendimiento(); $emprendimiento = $emprendimientoModel->findById($_GET['id']); // Debug logging temporal $logFile = __DIR__ . '/../../debug_emprendimiento_data.log'; $logData = date('Y-m-d H:i:s') . ' - getEmprendimiento ID: ' . $_GET['id'] . PHP_EOL; $logData .= 'Datos completos: ' . json_encode($emprendimiento, JSON_PRETTY_PRINT) . PHP_EOL; $logData .= '---' . PHP_EOL; file_put_contents($logFile, $logData, FILE_APPEND); if ($emprendimiento) { echo json_encode(['success' => true, 'emprendimiento' => $emprendimiento]); } else { echo json_encode(['success' => false, 'message' => 'Emprendimiento no encontrado']); } } public function proyectos() { require_once __DIR__ . '/../models/User.php'; require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/Emprendimiento.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $userModel = new User(); $projectModel = new Project(); $productModel = new Product(); $emprendimientoModel = new Emprendimiento(); $paTotalsModel = new ProjectAction(); $data = $this->getUserData(); $data['users'] = $userModel->getAll(); $data['admins_and_managers'] = $userModel->getAdminsAndManagers(); $data['projects'] = $projectModel->getAll(); $data['products'] = $productModel->getAll(); $data['emprendimientos'] = $emprendimientoModel->getAll(); $data['title'] = 'Gestión de Proyectos'; // Filtrado opcional por usuario y/o status (soporte para enlaces desde KPIs) $filterUserId = isset($_GET['user_id']) && $_GET['user_id'] !== '' ? (int)$_GET['user_id'] : null; $filterStatus = isset($_GET['status']) && $_GET['status'] !== '' ? (string)$_GET['status'] : null; if ($filterUserId !== null || $filterStatus !== null) { $data['projects'] = array_values(array_filter($data['projects'], function ($proj) use ($filterUserId, $filterStatus) { if ($filterUserId !== null && (int)($proj['user_id'] ?? 0) !== $filterUserId) return false; if ($filterStatus !== null && ($proj['status'] ?? '') !== $filterStatus) return false; return true; })); $data['active_filters'] = [ 'user_id' => $filterUserId, 'status' => $filterStatus ]; } // Asociar imagen y precio de producto a cada proyecto $productImages = []; $productPrices = []; foreach ($data['products'] as $prod) { $productImages[$prod['producto']] = $prod['imagen']; $productPrices[$prod['producto']] = isset($prod['precio']) ? (int)$prod['precio'] : 0; } require_once __DIR__ . '/../models/PaidProject.php'; $paidProjectModel = new PaidProject(); foreach ($data['projects'] as &$proj) { $img = isset($productImages[$proj['product']]) && $productImages[$proj['product']] ? $productImages[$proj['product']] : null; $proj['product_image'] = $img ? (Helper::asset('uploads/products/' . $img)) : Helper::asset('assets/img/emprendedor.gif'); $proj['product_price'] = isset($productPrices[$proj['product']]) ? (int)$productPrices[$proj['product']] : 0; // Calcular días restantes if (!empty($proj['end_date'])) { $hoy = new DateTime(); $end = new DateTime($proj['end_date']); $diff = $hoy->diff($end); $dias = (int)$diff->format('%r%a'); $proj['dias_restantes'] = $dias; if ($dias > 10) { $proj['dias_color'] = 'success'; // verde } elseif ($dias >= 4) { $proj['dias_color'] = 'warning'; // amarillo } else { $proj['dias_color'] = 'danger'; // rojo } } else { $proj['dias_restantes'] = null; $proj['dias_color'] = 'secondary'; } // Calcular total de días del proyecto (si tiene inicio y fin) if (!empty($proj['start_date']) && !empty($proj['end_date'])) { $start = new DateTime($proj['start_date']); $end = new DateTime($proj['end_date']); $totalDiff = $start->diff($end); $proj['total_dias'] = (int)$totalDiff->format('%r%a'); } else { $proj['total_dias'] = null; } // Totales de acciones: fases y horas try { $totals = $paTotalsModel->getTotalsByProjectId($proj['id']); $proj['phases_count'] = (int)($totals['phases_count'] ?? 0); $proj['actions_count'] = (int)($totals['actions_count'] ?? 0); $proj['actions_done'] = (int)($totals['actions_done'] ?? 0); $proj['total_hours'] = (int)($totals['total_hours'] ?? 0); $proj['total_phase_days'] = (int)($totals['total_phase_days'] ?? 0); $proj['total_price_actions'] = (int)($totals['total_price'] ?? 0); $summary = $paidProjectModel->getSummaryByProjectId($proj['id']); $latest = $summary['latest'] ?? null; $proj['paid_amount'] = (int)($summary['total_amount'] ?? $proj['paid_amount'] ?? 0); $proj['paid_observations'] = $latest['observations'] ?? null; $proj['paid_updated_at'] = $latest['updated_at'] ?? $latest['created_at'] ?? null; $proj['paid_entries_count'] = (int)($summary['entries_count'] ?? 0); } catch (Throwable $e) { $proj['phases_count'] = 0; $proj['actions_count'] = 0; $proj['actions_done'] = 0; $proj['total_hours'] = 0; $proj['total_phase_days'] = 0; $proj['total_price_actions'] = 0; $proj['paid_observations'] = null; $proj['paid_updated_at'] = null; $proj['paid_entries_count'] = 0; } } unset($proj); // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_project_action': require_once __DIR__ . '/../models/ProjectAction.php'; $paModel = new ProjectAction(); $payload = [ 'project_id' => $_POST['project_id'], 'fase_num' => $_POST['fase_num'] ?? null, 'fase_nombre' => $_POST['fase_nombre'] ?? null, 'accion_num' => $_POST['accion_num'] ?? null, 'accion' => $_POST['accion'] ?? null, 'horas' => isset($_POST['horas']) && $_POST['horas'] !== '' ? (int)$_POST['horas'] : null, 'dia' => isset($_POST['dia']) && $_POST['dia'] !== '' ? (int)$_POST['dia'] : null, 'status' => $_POST['status'] ?? 'Pendiente', 'inicio' => $_POST['inicio'] ?? null, 'fin' => $_POST['fin'] ?? null, 'especialista' => $_POST['especialista'] ?? null, 'observaciones' => $_POST['observaciones'] ?? null, ]; $ok = $paModel->create($payload); $_SESSION[$ok ? 'success' : 'error'] = $ok ? 'Acción creada' : 'No se pudo crear la acción'; break; case 'update_project_action': require_once __DIR__ . '/../models/ProjectAction.php'; $paModel = new ProjectAction(); $id = $_POST['id'] ?? null; if (!$id) { $_SESSION['error'] = 'ID requerido'; break; } $payload = [ 'project_id' => $_POST['project_id'], 'fase_num' => $_POST['fase_num'] ?? null, 'fase_nombre' => $_POST['fase_nombre'] ?? null, 'accion_num' => $_POST['accion_num'] ?? null, 'accion' => $_POST['accion'] ?? null, 'horas' => isset($_POST['horas']) && $_POST['horas'] !== '' ? (int)$_POST['horas'] : null, 'dia' => isset($_POST['dia']) && $_POST['dia'] !== '' ? (int)$_POST['dia'] : null, 'status' => $_POST['status'] ?? 'Pendiente', 'inicio' => $_POST['inicio'] ?? null, 'fin' => $_POST['fin'] ?? null, 'especialista' => $_POST['especialista'] ?? null, 'observaciones' => $_POST['observaciones'] ?? null, ]; $ok = $paModel->update($id, $payload); $_SESSION[$ok ? 'success' : 'error'] = $ok ? 'Acción actualizada' : 'No se pudo actualizar la acción'; break; case 'delete_project_action': require_once __DIR__ . '/../models/ProjectAction.php'; $paModel = new ProjectAction(); $id = $_POST['id'] ?? null; $ok = $id ? $paModel->delete($id) : false; $_SESSION[$ok ? 'success' : 'error'] = $ok ? 'Acción eliminada' : 'No se pudo eliminar la acción'; break; case 'create_project': // Respetar switch de uso de fases del producto $usePhases = isset($_POST['use_product_phases']); $productName = trim($_POST['product'] ?? ''); // Calcular end_date si está vacío y el switch está activo $startDate = $_POST['start_date'] ?? null; $endDate = $_POST['end_date'] ?? null; if ($usePhases && $productName !== '' && $startDate && empty($endDate)) { require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/ProductAction.php'; $pModelTmp = new Product(); $paTemplateTmp = new ProductAction(); $productRowTmp = $pModelTmp->getByProducto($productName); $daysToAdd = 0; if ($productRowTmp && isset($productRowTmp['id'])) { $templateActionsTmp = $paTemplateTmp->getByProductId($productRowTmp['id']); if (is_array($templateActionsTmp) && count($templateActionsTmp) > 0) { // tomar el mayor 'dia' como duración $maxDia = 0; foreach ($templateActionsTmp as $ta) { $d = isset($ta['dia']) && $ta['dia'] !== '' ? (int)$ta['dia'] : 0; if ($d > $maxDia) $maxDia = $d; } $daysToAdd = $maxDia; } } if ($daysToAdd <= 0 && isset($productRowTmp['dias'])) { $daysToAdd = (int)$productRowTmp['dias']; } if ($daysToAdd > 0) { // agregar días hábiles (excluye sábados y domingos) $current = new DateTime($startDate); $working = 0; while ($working < $daysToAdd) { $current->modify('+1 day'); $dow = (int)$current->format('w'); // 0 dom, 6 sab if ($dow !== 0 && $dow !== 6) { $working++; } } $endDate = $current->format('Y-m-d'); } } $paidAmount = isset($_POST['paid_amount']) && $_POST['paid_amount'] !== '' ? $_POST['paid_amount'] : 0; $projectData = [ 'user_id' => $_POST['user_id'], 'emprendimiento_id' => !empty($_POST['emprendimiento_id']) ? $_POST['emprendimiento_id'] : null, 'name' => $_POST['name'], 'description' => $_POST['description'], 'product' => $productName, 'project_manager' => $_POST['project_manager'], 'status' => 'Pendiente', 'start_date' => $startDate, 'end_date' => $endDate, 'progress' => 0, 'budget' => null, 'paid_amount' => $paidAmount ]; $newProjectId = $projectModel->create($projectData); if ($newProjectId) { if ($usePhases && $productName !== '') { // Clonar fases/acciones por defecto desde el producto seleccionado require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/ProductAction.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $pModel = new Product(); $paTemplate = new ProductAction(); $projActions = new ProjectAction(); $productRow = $pModel->getByProducto($productName); if ($productRow && isset($productRow['id'])) { $templateActions = $paTemplate->getByProductId($productRow['id']); if (is_array($templateActions)) { foreach ($templateActions as $ta) { $projActions->create([ 'project_id' => $newProjectId, 'fase_num' => $ta['fase'] ?? null, 'fase_nombre' => $ta['nombre'] ?? null, 'accion_num' => $ta['accion_num'] ?? null, 'accion' => $ta['accion'] ?? null, 'horas' => $ta['horas'] ?? null, 'dia' => $ta['dia'] ?? null, 'status' => 'Pendiente', 'inicio' => null, 'fin' => null, 'especialista' => null, 'observaciones' => null, ]); } } } } $_SESSION['success'] = 'Proyecto creado exitosamente'; // Notificar al usuario por chat try { require_once __DIR__ . '/../models/Chat.php'; $chat = new Chat(); $adminId = (int)($_SESSION['user_id'] ?? 0); $clientId = (int)($_POST['user_id'] ?? 0); if ($adminId > 0 && $clientId > 0) { $convId = $chat->ensureConversation($adminId, $clientId); $projId = (int)$newProjectId; $projName = trim($_POST['name'] ?? 'tu Proyecto'); $link = Helper::appUrl('clientes/proyectos') . '#proj=' . $projId; $msg = "🚀 ¡Nuevo Proyecto creado!\n[" . ($projName !== '' ? $projName : 'Proyecto') . "](" . $link . ")"; $chat->insertMessage($convId, $adminId, $msg, 'text', null); } } catch (Throwable $e) { /* noop */ } } else { $_SESSION['error'] = 'Error al crear el proyecto'; } break; case 'update_project': $existingProject = $projectModel->getById($_POST['project_id']); $existingStatus = $existingProject['status'] ?? 'Pendiente'; $existingBudget = array_key_exists('budget', (array)$existingProject) ? $existingProject['budget'] : null; $existingProgress = $existingProject['progress'] ?? 0; $paidAmount = isset($_POST['paid_amount']) && $_POST['paid_amount'] !== '' ? $_POST['paid_amount'] : ($existingProject['paid_amount'] ?? 0); $projectData = [ 'user_id' => $_POST['user_id'], 'emprendimiento_id' => !empty($_POST['emprendimiento_id']) ? $_POST['emprendimiento_id'] : null, 'name' => $_POST['name'], 'description' => $_POST['description'], 'product' => $_POST['product'], 'project_manager' => $_POST['project_manager'], 'status' => $existingStatus, 'start_date' => $_POST['start_date'], 'end_date' => $_POST['end_date'], 'progress' => $existingProgress, 'budget' => $existingBudget, 'paid_amount' => $paidAmount ]; $result = $projectModel->update($_POST['project_id'], $projectData); if ($result) { $_SESSION['success'] = 'Proyecto actualizado exitosamente'; // Notificar al usuario por chat try { require_once __DIR__ . '/../models/Chat.php'; $chat = new Chat(); $adminId = (int)($_SESSION['user_id'] ?? 0); $clientId = (int)($_POST['user_id'] ?? 0); if ($adminId > 0 && $clientId > 0) { $convId = $chat->ensureConversation($adminId, $clientId); $projId = (int)($_POST['project_id'] ?? 0); $projName = trim($_POST['name'] ?? 'tu Proyecto'); $link = $projId > 0 ? (Helper::appUrl('clientes/proyectos') . '#proj=' . $projId) : Helper::appUrl('clientes/proyectos'); $msg = "🛠️ Se actualizó tu Proyecto\n[" . ($projName !== '' ? $projName : 'Proyecto') . "](" . $link . ")"; $chat->insertMessage($convId, $adminId, $msg, 'text', null); } } catch (Throwable $e) { /* noop */ } } else { $_SESSION['error'] = 'Error al actualizar el proyecto'; } break; case 'delete_project': $result = $projectModel->delete($_POST['project_id']); if ($result) { $_SESSION['success'] = 'Proyecto eliminado exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar el proyecto'; } break; } header('Location: ' . Helper::appUrl('admin/proyectos')); exit; } $content = $this->renderViewToString('admin/proyectos', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function getProject() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/ProjectAction.php'; require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/PaidProject.php'; $projectModel = new Project(); $projectActionModel = new ProjectAction(); $productModel = new Product(); $project = $projectModel->getById($_GET['id']); if ($project) { // Asociar imagen del producto $product = $productModel->getByProducto($project['product']); $img = $product && !empty($product['imagen']) ? $product['imagen'] : null; $project['product_image'] = $img ? (Helper::asset('uploads/products/' . $img)) : Helper::asset('assets/img/emprendedor.gif'); $project['product_price'] = $product && isset($product['precio']) ? (int)$product['precio'] : 0; // Adjuntar acciones del proyecto $project['actions'] = $projectActionModel->getByProjectId($project['id']); $paidProjectModel = new PaidProject(); $summary = $paidProjectModel->getSummaryByProjectId($project['id']); $latest = $summary['latest'] ?? null; $totalAmount = (int)($summary['total_amount'] ?? $project['paid_amount'] ?? 0); $project['paid_amount'] = $totalAmount; $project['paid_info'] = [ 'amount' => $totalAmount, 'observations' => $latest['observations'] ?? null, 'updated_at' => $latest['updated_at'] ?? $latest['created_at'] ?? null, 'created_at' => $latest['created_at'] ?? null, 'entries_count' => (int)($summary['entries_count'] ?? 0) ]; try { $totals = $projectActionModel->getTotalsByProjectId($project['id']); $project['total_hours'] = (int)($totals['total_hours'] ?? 0); $project['phases_count'] = (int)($totals['phases_count'] ?? 0); $project['actions_count'] = (int)($totals['actions_count'] ?? 0); $project['actions_done'] = (int)($totals['actions_done'] ?? 0); $project['total_phase_days'] = (int)($totals['total_phase_days'] ?? 0); $project['total_price_actions'] = (int)($totals['total_price'] ?? 0); } catch (Throwable $e) { $project['total_hours'] = (int)($project['total_hours'] ?? 0); $project['phases_count'] = (int)($project['phases_count'] ?? 0); $project['actions_count'] = (int)($project['actions_count'] ?? 0); $project['actions_done'] = (int)($project['actions_done'] ?? 0); $project['total_phase_days'] = (int)($project['total_phase_days'] ?? 0); $project['total_price_actions'] = (int)($project['total_price_actions'] ?? 0); } echo json_encode(['success' => true, 'project' => $project]); } else { echo json_encode(['success' => false, 'message' => 'Proyecto no encontrado']); } } public function getProductData() { if (!isset($_GET['product'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Producto no proporcionado']); return; } require_once __DIR__ . '/../models/Product.php'; $productModel = new Product(); $product = $productModel->getByProducto($_GET['product']); if ($product) { echo json_encode(['success' => true, 'product' => $product]); } else { echo json_encode(['success' => false, 'message' => 'Producto no encontrado']); } } public function getEmprendimientosByUser() { if (!isset($_GET['user_id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de usuario no proporcionado']); return; } require_once __DIR__ . '/../models/Emprendimiento.php'; $emprendimientoModel = new Emprendimiento(); $emprendimientos = $emprendimientoModel->getByUser($_GET['user_id']); echo json_encode(['success' => true, 'emprendimientos' => $emprendimientos]); } public function productos() { require_once __DIR__ . '/../models/Product.php'; $productModel = new Product(); $data = $this->getUserData(); $data['products'] = $productModel->getAll(); $data['title'] = 'Gestión de Productos'; // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_product': // Manejar subida de imagen $imagenNombre = null; if (isset($_FILES['imagen']) && $_FILES['imagen']['error'] === UPLOAD_ERR_OK && $_FILES['imagen']['size'] > 0) { $ext = pathinfo($_FILES['imagen']['name'], PATHINFO_EXTENSION); $allowed = ['jpg','jpeg','png','webp']; $maxSize = 5 * 1024 * 1024; if (!in_array(strtolower($ext), $allowed)) { $_SESSION['error'] = 'Tipo de imagen no permitido.'; break; } if ($_FILES['imagen']['size'] > $maxSize) { $_SESSION['error'] = 'La imagen excede 5MB.'; break; } $slug = preg_replace('/[^a-z0-9\-]+/i', '-', $_POST['producto']); $slug = trim(preg_replace('/\-+/', '-', $slug), '-'); $imagenNombre = ($slug ?: 'producto') . '_' . time() . '.' . $ext; $dest = __DIR__ . '/../../uploads/products/' . $imagenNombre; if (!is_dir(__DIR__ . '/../../uploads/products/')) { mkdir(__DIR__ . '/../../uploads/products/', 0777, true); } move_uploaded_file($_FILES['imagen']['tmp_name'], $dest); } $productData = [ 'linea' => $_POST['linea'], 'producto' => $_POST['producto'], 'status' => $_POST['status'], 'premisa' => $_POST['premisa'], 'beneficios' => $_POST['beneficios'], 'proposito_1' => $_POST['proposito_1'], 'desarrollo_1' => $_POST['desarrollo_1'], 'proposito_2' => $_POST['proposito_2'], 'desarrollo_2' => $_POST['desarrollo_2'], 'proposito_3' => $_POST['proposito_3'], 'desarrollo_3' => $_POST['desarrollo_3'], 'horas' => $_POST['horas'], 'dias' => $_POST['dias'], 'precio' => $_POST['precio'], 'proyecto' => $_POST['proyecto'], 'imagen' => $imagenNombre ?? '', 'informacion' => $_POST['informacion'], 'apuntes' => $_POST['apuntes'], 'plantilla_1' => $_POST['plantilla_1'], 'plantilla_2' => $_POST['plantilla_2'], 'condiciones' => $_POST['condiciones'], 'observaciones' => $_POST['observaciones'] ]; $result = $productModel->create($productData); if ($result) { $_SESSION['success'] = 'Producto creado exitosamente'; } else { $_SESSION['error'] = 'Error al crear el producto'; } break; case 'update_product': // Subida opcional de nueva imagen $imagenActual = $_POST['imagen_actual'] ?? ''; $imagenFinal = $imagenActual; if (isset($_FILES['imagen']) && $_FILES['imagen']['error'] === UPLOAD_ERR_OK && $_FILES['imagen']['size'] > 0) { $ext = pathinfo($_FILES['imagen']['name'], PATHINFO_EXTENSION); $allowed = ['jpg','jpeg','png','webp']; $maxSize = 5 * 1024 * 1024; if (!in_array(strtolower($ext), $allowed)) { $_SESSION['error'] = 'Tipo de imagen no permitido.'; break; } if ($_FILES['imagen']['size'] > $maxSize) { $_SESSION['error'] = 'La imagen excede 5MB.'; break; } $slug = preg_replace('/[^a-z0-9\-]+/i', '-', $_POST['producto']); $slug = trim(preg_replace('/\-+/', '-', $slug), '-'); $newName = ($slug ?: 'producto') . '_' . time() . '.' . $ext; $dest = __DIR__ . '/../../uploads/products/' . $newName; if (!is_dir(__DIR__ . '/../../uploads/products/')) { mkdir(__DIR__ . '/../../uploads/products/', 0777, true); } if (move_uploaded_file($_FILES['imagen']['tmp_name'], $dest)) { // Borrar la imagen anterior si existía if (!empty($imagenActual)) { $oldPublic = Helper::asset('uploads/products/' . $imagenActual); Helper::deleteFileIfExists($oldPublic); $oldFs = __DIR__ . '/../../uploads/products/' . $imagenActual; if (file_exists($oldFs) && is_file($oldFs)) {@unlink($oldFs);} } $imagenFinal = $newName; } } $productData = [ 'linea' => $_POST['linea'], 'producto' => $_POST['producto'], 'status' => $_POST['status'], 'premisa' => $_POST['premisa'], 'beneficios' => $_POST['beneficios'], 'proposito_1' => $_POST['proposito_1'], 'desarrollo_1' => $_POST['desarrollo_1'], 'proposito_2' => $_POST['proposito_2'], 'desarrollo_2' => $_POST['desarrollo_2'], 'proposito_3' => $_POST['proposito_3'], 'desarrollo_3' => $_POST['desarrollo_3'], 'horas' => $_POST['horas'], 'dias' => $_POST['dias'], 'precio' => $_POST['precio'], 'proyecto' => $_POST['proyecto'], 'imagen' => $imagenFinal, 'informacion' => $_POST['informacion'], 'apuntes' => $_POST['apuntes'], 'plantilla_1' => $_POST['plantilla_1'], 'plantilla_2' => $_POST['plantilla_2'], 'condiciones' => $_POST['condiciones'], 'observaciones' => $_POST['observaciones'] ]; $result = $productModel->update($_POST['product_id'], $productData); if ($result) { $_SESSION['success'] = 'Producto actualizado exitosamente'; } else { $_SESSION['error'] = 'Error al actualizar el producto'; } break; case 'delete_product': $result = $productModel->delete($_POST['product_id']); if ($result) { $_SESSION['success'] = 'Producto eliminado exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar el producto'; } break; } header('Location: ' . Helper::appUrl('admin/productos')); exit; } $content = $this->renderViewToString('admin/productos', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function getProduct() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/ProductAction.php'; $productModel = new Product(); $product = $productModel->getById($_GET['id']); if ($product) { // Adjuntar acciones del producto (compatibilidad: asignamos en 'phases') $actionModel = new ProductAction(); $phases = $actionModel->getByProductId($product['id']); $product['phases'] = $phases; // Calcular horas y días dinámicos desde product_actions $totalHoras = 0; $maxDia = 0; foreach ($phases as $a) { $h = isset($a['horas']) && $a['horas'] !== '' ? (int)$a['horas'] : 0; $totalHoras += $h; $d = isset($a['dia']) && $a['dia'] !== '' ? (int)$a['dia'] : 0; if ($d > $maxDia) $maxDia = $d; } $product['horas'] = $totalHoras; $product['dias'] = $maxDia; echo json_encode(['success' => true, 'product' => $product]); } else { echo json_encode(['success' => false, 'message' => 'Producto no encontrado']); } } // CRUD para acciones de producto public function productActions() { require_once __DIR__ . '/../models/ProductAction.php'; header('Content-Type: application/json'); $method = $_SERVER['REQUEST_METHOD']; $actionModel = new ProductAction(); try { if ($method === 'GET') { if (isset($_GET['id'])) { $row = $actionModel->getById($_GET['id']); echo json_encode(['success' => (bool)$row, 'action' => $row]); return; } elseif (isset($_GET['product_id'])) { $rows = $actionModel->getByProductId($_GET['product_id']); echo json_encode(['success' => true, 'actions' => $rows]); return; } http_response_code(400); echo json_encode(['success' => false, 'message' => 'Parámetros inválidos']); return; } if ($method === 'POST') { $op = $_POST['op'] ?? ''; if ($op === 'create') { $payload = [ 'product_id' => $_POST['product_id'], 'fase_num' => $_POST['fase_num'] ?? null, 'fase_nombre' => $_POST['fase_nombre'] ?? null, 'accion_num' => $_POST['accion_num'] ?? null, 'accion' => $_POST['accion'] ?? null, 'horas' => isset($_POST['horas']) && $_POST['horas'] !== '' ? (int)$_POST['horas'] : null, 'dia' => isset($_POST['dia']) && $_POST['dia'] !== '' ? (int)$_POST['dia'] : null, ]; $ok = $actionModel->create($payload); echo json_encode(['success' => (bool)$ok]); return; } elseif ($op === 'update') { $id = $_POST['id'] ?? null; if (!$id) { echo json_encode(['success' => false, 'message' => 'ID requerido']); return; } $payload = [ 'product_id' => $_POST['product_id'], 'fase_num' => $_POST['fase_num'] ?? null, 'fase_nombre' => $_POST['fase_nombre'] ?? null, 'accion_num' => $_POST['accion_num'] ?? null, 'accion' => $_POST['accion'] ?? null, 'horas' => isset($_POST['horas']) && $_POST['horas'] !== '' ? (int)$_POST['horas'] : null, 'dia' => isset($_POST['dia']) && $_POST['dia'] !== '' ? (int)$_POST['dia'] : null, ]; $ok = $actionModel->update($id, $payload); echo json_encode(['success' => (bool)$ok]); return; } elseif ($op === 'delete') { $id = $_POST['id'] ?? null; $ok = $id ? $actionModel->delete($id) : false; echo json_encode(['success' => (bool)$ok]); return; } elseif ($op === 'reorder') { $faseNum = $_POST['fase_num'] ?? null; $faseNombre = $_POST['fase_nombre'] ?? null; $ordered = $_POST['ordered_ids'] ?? []; if (!is_array($ordered)) { $ordered = []; } $ok = $actionModel->reorderList($faseNum, $faseNombre, $ordered); echo json_encode(['success' => (bool)$ok]); return; } http_response_code(400); echo json_encode(['success' => false, 'message' => 'Operación inválida']); return; } http_response_code(405); echo json_encode(['success' => false, 'message' => 'Método no permitido']); } catch (Throwable $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error interno']); } } // CRUD simple para acciones de proyecto public function projectActions() { require_once __DIR__ . '/../models/ProjectAction.php'; header('Content-Type: application/json'); // Verificar autenticación específicamente para este endpoint if (!isset($_SESSION['user_id'])) { http_response_code(401); error_log("ProjectActions - No user session"); echo json_encode(['success' => false, 'message' => 'No autenticado']); return; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $role = $userModel->getRole($_SESSION['user_id']); if ($role !== 'admin') { http_response_code(403); error_log("ProjectActions - User role: $role (not admin)"); echo json_encode(['success' => false, 'message' => 'Sin permisos de administrador']); return; } $method = $_SERVER['REQUEST_METHOD']; $actionModel = new ProjectAction(); try { if ($method === 'GET') { if (isset($_GET['id'])) { $row = $actionModel->getById($_GET['id']); echo json_encode(['success' => (bool)$row, 'action' => $row]); } elseif (isset($_GET['project_id'])) { $rows = $actionModel->getByProjectId($_GET['project_id']); echo json_encode(['success' => true, 'actions' => $rows]); } else { http_response_code(400); error_log("ProjectActions - GET request without valid params"); echo json_encode(['success' => false, 'message' => 'Parámetros inválidos - GET sin id o project_id']); } return; } if ($method === 'POST') { $payload = $_POST; error_log("ProjectActions - POST payload: " . json_encode($payload)); // Verificar si el POST está vacío if (empty($payload)) { http_response_code(400); error_log("ProjectActions - POST request with empty payload"); echo json_encode(['success' => false, 'message' => 'Datos POST vacíos']); return; } $sanitizeCurrency = function($value) { if ($value === null) { return null; } if (is_numeric($value)) { return (int)round((float)$value); } $str = trim((string)$value); if ($str === '') { return null; } $normalized = preg_replace('/[^0-9\-]/', '', $str); if ($normalized === '' || $normalized === '-') { return null; } return (int)$normalized; }; // Normalizaciones básicas $payload['horas'] = isset($payload['horas']) && $payload['horas'] !== '' ? (int)$payload['horas'] : null; $payload['dia'] = isset($payload['dia']) && $payload['dia'] !== '' ? (int)$payload['dia'] : null; if (array_key_exists('precio', $payload)) { $payload['precio'] = $sanitizeCurrency($payload['precio']); } // Normalizar campos de texto vacíos a null para evitar sobrescribir con vacíos if (isset($payload['fase_num']) && $payload['fase_num'] === '') unset($payload['fase_num']); if (isset($payload['fase_nombre']) && trim($payload['fase_nombre']) === '') unset($payload['fase_nombre']); if (isset($payload['accion_num']) && $payload['accion_num'] === '') unset($payload['accion_num']); if (isset($payload['accion']) && trim($payload['accion']) === '') unset($payload['accion']); if (isset($payload['especialista']) && trim($payload['especialista']) === '') $payload['especialista'] = null; if (isset($payload['observaciones']) && trim($payload['observaciones']) === '') $payload['observaciones'] = null; if (isset($payload['inicio']) && trim($payload['inicio']) === '') $payload['inicio'] = null; if (isset($payload['fin']) && trim($payload['fin']) === '') $payload['fin'] = null; $projectStatus = null; $op = $payload['op'] ?? 'create'; if ($op === 'create') { if (empty($payload['especialista'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'El campo Especialista es obligatorio']); return; } // Calcular fecha fin automáticamente si hay inicio y horas pero no hay fin válido if (!empty($payload['inicio']) && !empty($payload['horas']) && (empty($payload['fin']) || $payload['fin'] === null)) { try { $inicioDate = new DateTime($payload['inicio']); $horas = (int)$payload['horas']; $inicioDate->modify("+{$horas} hours"); $payload['fin'] = $inicioDate->format('Y-m-d H:i:s'); } catch (Exception $e) { // Si falla el cálculo, continuar sin fecha fin } } $ok = $actionModel->create($payload); if ($ok) { $projectId = isset($payload['project_id']) ? (int)$payload['project_id'] : null; if ($projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } } echo json_encode(['success' => (bool)$ok, 'project_status' => $projectStatus]); return; } elseif ($op === 'update') { if (empty($payload['id'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID requerido']); return; } if (empty($payload['especialista'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'El campo Especialista es obligatorio']); return; } // Calcular fecha fin automáticamente si hay inicio y horas pero no hay fin válido if (!empty($payload['inicio']) && !empty($payload['horas']) && (empty($payload['fin']) || $payload['fin'] === null)) { try { $inicioDate = new DateTime($payload['inicio']); $horas = (int)$payload['horas']; $inicioDate->modify("+{$horas} hours"); $payload['fin'] = $inicioDate->format('Y-m-d H:i:s'); } catch (Exception $e) { // Si falla el cálculo, continuar sin fecha fin } } $ok = $actionModel->update($payload['id'], $payload); if ($ok) { $projectId = null; if (isset($payload['project_id']) && $payload['project_id'] !== '') { $projectId = (int)$payload['project_id']; } else { $projectId = $actionModel->getProjectIdByAction($payload['id']); } if ($projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } } echo json_encode(['success' => (bool)$ok, 'project_status' => $projectStatus]); return; } elseif ($op === 'update_status') { if (empty($payload['id'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID requerido']); return; } $status = isset($payload['status']) ? trim((string)$payload['status']) : ''; $allowedStatuses = ['Pendiente', 'En Progreso', 'Lograda']; if (!in_array($status, $allowedStatuses, true)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Estado inválido']); return; } $projectId = $actionModel->getProjectIdByAction($payload['id']); $ok = $actionModel->updateStatus($payload['id'], $status); if ($ok && $projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } echo json_encode(['success' => (bool)$ok, 'project_status' => $projectStatus]); return; } elseif ($op === 'update_field') { // Actualización inline de un solo campo if (empty($payload['id'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID requerido']); return; } if (empty($payload['field'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'Campo requerido']); return; } $actionId = $payload['id']; $field = $payload['field']; $value = isset($payload['value']) ? $payload['value'] : ''; // Log para debugging error_log("ProjectActions update_field - ID: $actionId, Field: $field, Value: $value"); // Campos permitidos para edición inline $allowedFields = ['accion_num', 'accion', 'horas', 'especialista', 'precio']; if (!in_array($field, $allowedFields, true)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Campo no permitido: ' . $field]); return; } // Verificar que la acción existe $currentAction = $actionModel->getById($actionId); if (!$currentAction) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Acción no encontrada']); return; } // Preparar datos según el campo $updateData = []; if ($field === 'horas' || $field === 'accion_num') { $updateData[$field] = ($value !== '' && $value !== null) ? (int)$value : null; error_log("ProjectActions - Campo numérico $field: " . var_export($updateData[$field], true)); // Si se actualizan las horas, recalcular fecha fin automáticamente if ($field === 'horas' && $updateData[$field] > 0) { if ($currentAction && !empty($currentAction['inicio'])) { try { $inicioDate = new DateTime($currentAction['inicio']); $inicioDate->modify("+{$updateData[$field]} hours"); $updateData['fin'] = $inicioDate->format('Y-m-d H:i:s'); error_log("ProjectActions - Fecha fin calculada: " . $updateData['fin']); } catch (Exception $e) { error_log("ProjectActions - Error calculando fecha fin: " . $e->getMessage()); } } } } elseif ($field === 'precio') { $updateData[$field] = $sanitizeCurrency($value); error_log("ProjectActions - Precio sanitizado: " . var_export($updateData[$field], true)); } else { $updateData[$field] = trim($value); error_log("ProjectActions - Campo texto $field: " . var_export($updateData[$field], true)); } error_log("ProjectActions - Datos para actualizar: " . json_encode($updateData)); $projectStatus = null; $ok = $actionModel->updatePartial($actionId, $updateData); error_log("ProjectActions - Resultado updatePartial: " . var_export($ok, true)); if ($ok) { $projectId = $actionModel->getProjectIdByAction($actionId); if ($projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } echo json_encode(['success' => true, 'project_status' => $projectStatus]); } else { echo json_encode(['success' => false, 'message' => 'No se pudo actualizar el campo']); } return; } elseif ($op === 'update_dates') { // Actualización de fecha inicio con cálculo automático de fecha fin basado en horas if (empty($payload['id'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID requerido']); return; } $actionId = $payload['id']; $inicio = isset($payload['inicio']) ? $payload['inicio'] : null; $horas = isset($payload['horas']) ? (int)$payload['horas'] : 0; // Calcular fecha fin sumando las horas a la fecha de inicio $fin = null; if ($inicio && $horas > 0) { try { $inicioDate = new DateTime($inicio); $inicioDate->modify("+{$horas} hours"); $fin = $inicioDate->format('Y-m-d H:i:s'); } catch (Exception $e) { // Si falla el cálculo, solo actualizar inicio } } // Actualizar ambas fechas $updateData = ['inicio' => $inicio]; if ($fin !== null) { $updateData['fin'] = $fin; } $projectStatus = null; $ok = $actionModel->updatePartial($actionId, $updateData); if ($ok) { $projectId = $actionModel->getProjectIdByAction($actionId); if ($projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } } echo json_encode(['success' => (bool)$ok, 'project_status' => $projectStatus]); return; } elseif ($op === 'delete') { if (empty($payload['id'])) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID requerido']); return; } $projectId = $actionModel->getProjectIdByAction($payload['id']); $ok = $actionModel->delete($payload['id']); if ($ok && $projectId) { $projectStatus = $actionModel->syncProjectStatusWithActions($projectId); } echo json_encode(['success' => (bool)$ok, 'project_status' => $projectStatus]); return; } elseif ($op === 'reorder') { // Espera: project_id, fase_num, fase_nombre y ordered_ids[] $projectId = isset($payload['project_id']) ? (int)$payload['project_id'] : null; $faseNum = isset($payload['fase_num']) ? (int)$payload['fase_num'] : null; $faseNombre = $payload['fase_nombre'] ?? null; $ordered = isset($payload['ordered_ids']) ? (array)$payload['ordered_ids'] : []; $ok = $actionModel->reorderList($projectId, $faseNum, $faseNombre, $ordered); echo json_encode(['success' => (bool)$ok]); return; } http_response_code(400); echo json_encode(['success' => false, 'message' => 'Operación inválida']); return; } http_response_code(405); echo json_encode(['success' => false, 'message' => 'Método no permitido']); } catch (Throwable $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error interno']); } } // Importador desde CSV a la tabla product_actions (legacy - deshabilitado para producción) /* public function importProductPhases() { header('Content-Type: text/plain; charset=utf-8'); require_once __DIR__ . '/../models/ProductAction.php'; require_once __DIR__ . '/../models/Product.php'; $csvPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'fases_productos.csv'; if (!file_exists($csvPath)) { echo "CSV no encontrado en: {$csvPath}\n"; return; } $phaseModel = new ProductAction(); $productModel = new Product(); $handle = fopen($csvPath, 'r'); if (!$handle) { echo "No se pudo abrir el CSV\n"; return; } // Leer encabezados $headers = fgetcsv($handle); // Mapeo flexible por nombre de columna $map = array_flip($headers); $numInserts = 0; $perProductInserted = []; $perProductCleared = []; // Utilidad de normalización $toDecimal = function($str) { if ($str === null) return null; $s = trim((string)$str); if ($s === '') return null; // Quitar símbolos $, espacios, puntos de miles y convertir coma decimal a punto $s = str_replace(['$', ' ', ' '], '', $s); $s = str_replace(['.', ' días', ' día'], ['', '', ''], $s); $s = str_replace([','], ['.'], $s); // Si queda algo como "1.234.56" lo simplificamos: conservar solo el último punto como decimal if (substr_count($s, '.') > 1) { $parts = explode('.', $s); $dec = array_pop($parts); $s = implode('', $parts) . '.' . $dec; } if (!is_numeric($s)) return null; return (float)$s; }; while (($row = fgetcsv($handle)) !== false) { // Obtener campos según encabezados del CSV compartido // Posibles encabezados antiguos: product_id, Fase, Acciones, Horas, Días, Precio, Observaciones // Nuevos recomendados: product_id, fase_num, fase_nombre, accion_num, accion, horas $productId = isset($map['product_id']) ? $row[$map['product_id']] : null; if (!$productId || !is_numeric($productId)) continue; // Si es la primera fila de este producto, limpiar fases previas if (!isset($perProductCleared[$productId])) { $phaseModel->deleteByProduct($productId); $perProductCleared[$productId] = true; } // Nuevos nombres directos $faseNumDirect = isset($map['fase_num']) ? trim($row[$map['fase_num']]) : null; $faseNombreDirect= isset($map['fase_nombre']) ? trim($row[$map['fase_nombre']]) : null; $accionNumDirect = isset($map['accion_num']) ? trim($row[$map['accion_num']]) : null; $accionDirect = isset($map['accion']) ? trim($row[$map['accion']]) : null; $horasStr = isset($map['horas']) ? $row[$map['horas']] : (isset($map['Horas']) ? $row[$map['Horas']] : null); $diaStr = isset($map['dia']) ? $row[$map['dia']] : (isset($map['Día']) ? $row[$map['Día']] : (isset($map['Días']) ? $row[$map['Días']] : null)); // Soporte legacy $faseLabel = isset($map['Fase']) ? trim($row[$map['Fase']]) : null; $acciones = isset($map['Acciones']) ? trim($row[$map['Acciones']]) : null; // Derivar numero y nombre de fase a partir de la etiqueta flexible (ej. "1. Aplicación" o "Exploración") $faseNum = null; $faseNombre = null; if ($faseLabel !== null && $faseLabel !== '') { // Intentar: "N. Nombre" if (preg_match('/^\s*(\d+)[\.)\-]?\s*(.*)$/u', $faseLabel, $m)) { $faseNum = (int)$m[1]; $faseNombre = trim($m[2]); } else { $faseNombre = $faseLabel; } } $horas = $toDecimal($horasStr); $dia = null; if ($diaStr !== null && $diaStr !== '') { $diaNum = $toDecimal($diaStr); $dia = $diaNum !== null ? (int)round($diaNum) : null; } // Acción # $accionNum = null; if ($accionNumDirect !== null && $accionNumDirect !== '') { $accionNum = (int)$accionNumDirect; } if ($accionNum === null && $acciones) { // Si viene legacy sin numero de acción, secuenciamos $accionNum = ($perProductInserted[$productId] ?? 0) + 1; } $phaseModel->create([ 'product_id' => (int)$productId, 'fase_num' => $faseNum, 'fase_nombre' => $faseNombre, 'accion_num' => $accionNum, 'accion' => ($accionDirect !== null && $accionDirect !== '') ? $accionDirect : $acciones, 'horas' => $horas, 'dia' => $dia, ]); $numInserts++; $perProductInserted[$productId] = ($perProductInserted[$productId] ?? 0) + 1; } fclose($handle); echo "Importación completada. Registros insertados: {$numInserts}\n"; foreach ($perProductInserted as $pid => $count) { echo "Producto {$pid}: {$count} acciones\n"; } } */ public function actividades() { // Se renombró a Plan de Acción. Mantener compatibilidad mostrando la nueva vista. return $this->plan_accion(); } public function plan_accion() { require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $projectModel = new Project(); $actionModel = new ProjectAction(); $data = $this->getUserData(); $data['title'] = 'Plan de Acción'; // Cargar todos los proyectos (con cliente, PM y emprendimiento ya unidos en Project::getAll) $projects = $projectModel->getAll(); // Adjuntar acciones a cada proyecto foreach ($projects as &$p) { try { $p['actions'] = $actionModel->getByProjectId($p['id']); } catch (Throwable $e) { $p['actions'] = []; } } unset($p); $data['projects'] = $projects; $content = $this->renderViewToString('admin/plan_accion', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function getActivity() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Activity.php'; $activityModel = new Activity(); $activity = $activityModel->getById($_GET['id']); if ($activity) { // Obtener invitaciones $invitations = $activityModel->getInvitations($_GET['id']); $activity['invitations'] = $invitations; echo json_encode(['success' => true, 'activity' => $activity]); } else { echo json_encode(['success' => false, 'message' => 'Actividad no encontrada']); } } public function pagos() { require_once __DIR__ . '/../models/Payment.php'; require_once __DIR__ . '/../models/User.php'; require_once __DIR__ . '/../models/Emprendimiento.php'; require_once __DIR__ . '/../models/PaymentDetail.php'; $paymentModel = new Payment(); $userModel = new User(); $emprModel = new Emprendimiento(); $data = $this->getUserData(); $data['payments'] = $paymentModel->getAll(); $data['users'] = $userModel->getAll(); $data['title'] = 'Gestión de Pagos'; // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_payment': $comprobanteFile = $_FILES['comprobante'] ?? null; $comprobanteName = null; $comprobanteError = null; if ($comprobanteFile && $comprobanteFile['error'] === UPLOAD_ERR_OK) { $ext = pathinfo($comprobanteFile['name'], PATHINFO_EXTENSION); $allowed = ['jpg','jpeg','png','pdf','webp']; $maxSize = 5 * 1024 * 1024; // 5MB if (!in_array(strtolower($ext), $allowed)) { $comprobanteError = 'Tipo de archivo no permitido.'; } elseif ($comprobanteFile['size'] > $maxSize) { $comprobanteError = 'El archivo excede el tamaño máximo de 5MB.'; } else { $comprobanteName = 'comprobante_' . time() . '_' . rand(1000,9999) . '.' . $ext; $dest = __DIR__ . '/../../uploads/pagos/' . $comprobanteName; move_uploaded_file($comprobanteFile['tmp_name'], $dest); } } if ($comprobanteError) { $_SESSION['error'] = $comprobanteError; header('Location: ' . Helper::appUrl('admin/pagos')); exit; } // Modo Nota de Cobro $isNota = (isset($_POST['is_nota_cobro']) && $_POST['is_nota_cobro'] == '1'); $paymentMethod = $isNota ? 'Nota de Cobro' : ($_POST['payment_method'] ?? ''); // Para Nota de Cobro permitir elegir estado y por defecto 'Emitida' $status = $isNota ? ($_POST['status'] ?? 'Emitida') : ($_POST['status'] ?? 'Pendiente'); $payment_date = $_POST['payment_date'] ?: ($isNota ? date('Y-m-d') : null); $amount = isset($_POST['amount']) ? (float)$_POST['amount'] : 0; $emprendimiento_id = !empty($_POST['emprendimiento_id']) ? (int)$_POST['emprendimiento_id'] : null; $items = []; $itemsSum = 0; if ($isNota) { $raw = $_POST['nota_items_json'] ?? '[]'; error_log('AdminController CREATE: nota_items_json raw: ' . $raw); $parsed = json_decode($raw, true); error_log('AdminController CREATE: parsed items: ' . print_r($parsed, true)); if (is_array($parsed)) { foreach ($parsed as $it) { $desc = trim($it['description'] ?? ''); $qty = (float)($it['quantity'] ?? 0); $price = (float)($it['price'] ?? 0); $sub = (float)($it['subtotal'] ?? ($qty * $price)); if ($desc === '' || $qty <= 0) continue; $items[] = [ 'description' => $desc, 'quantity' => $qty, 'price' => $price, 'subtotal' => $sub, ]; $itemsSum += $sub; } } error_log('AdminController CREATE: final items array: ' . print_r($items, true)); error_log('AdminController CREATE: items count: ' . count($items)); } $subtotal = $isNota ? ( ($itemsSum > 0) ? $itemsSum : $amount ) : ($amount); // Datos adicionales de la Nota $consecutive = trim($_POST['consecutive'] ?? '') ?: null; $issue_city = trim($_POST['issue_city'] ?? '') ?: null; $issue_date = $_POST['issue_date'] ?? null; $due_date = $_POST['due_date'] ?? null; $service_start_date = $_POST['service_start_date'] ?? null; $service_end_date = $_POST['service_end_date'] ?? null; $service_city = trim($_POST['service_city'] ?? '') ?: null; $iva_percent = isset($_POST['iva_percent']) ? (float)$_POST['iva_percent'] : 0.0; $retefuente_percent = isset($_POST['retefuente_percent']) ? (float)$_POST['retefuente_percent'] : 0.0; $ica_percent = isset($_POST['ica_percent']) ? (float)$_POST['ica_percent'] : 0.0; // Calcular montos de impuestos/retenciones con redondeo entero $iva_amount = isset($_POST['iva_amount']) ? (float)$_POST['iva_amount'] : round($subtotal * $iva_percent / 100); $retefuente_amount = isset($_POST['retefuente_amount']) ? (float)$_POST['retefuente_amount'] : round($subtotal * $retefuente_percent / 100); $ica_amount = isset($_POST['ica_amount']) ? (float)$_POST['ica_amount'] : round($subtotal * $ica_percent / 100); $total = $subtotal + $iva_amount - $retefuente_amount - $ica_amount; if ($isNota) { $amount = $total; } // DOCX deshabilitado: vista previa 100% digital $notaDocName = null; $paymentData = [ 'user_id' => $_POST['user_id'], 'amount' => $amount, 'payment_method' => $paymentMethod, 'reference' => $_POST['reference'] ?? '', 'description' => $_POST['description'] ?? '', 'status' => $status, 'payment_date' => $payment_date, 'comprobante' => $comprobanteName, 'is_nota_cobro' => $isNota ? 1 : 0, 'nota_cobro_doc' => $notaDocName, 'subtotal' => $subtotal, 'total' => $total, 'emprendimiento_id' => $emprendimiento_id, 'consecutive' => $consecutive, 'issue_city' => $issue_city, 'issue_date' => $issue_date ?: date('Y-m-d'), 'due_date' => $due_date, 'service_start_date' => $service_start_date, 'service_end_date' => $service_end_date, 'service_city' => $service_city, 'iva_percent' => $iva_percent, 'iva_amount' => $iva_amount, 'retefuente_percent' => $retefuente_percent, 'retefuente_amount' => $retefuente_amount, 'ica_percent' => $ica_percent, 'ica_amount' => $ica_amount, ]; try { $insertId = $paymentModel->create($paymentData); if ($insertId) { // Guardar detalles si hay if ($isNota && !empty($items)) { error_log('AdminController CREATE: About to save items for payment ID: ' . $insertId); $pd = new PaymentDetail(); $result = $pd->replaceForPayment((int)$insertId, $items); error_log('AdminController CREATE: replaceForPayment result: ' . ($result ? 'SUCCESS' : 'FAILED')); } else { error_log('AdminController CREATE: Not saving items - isNota: ' . ($isNota ? 'true' : 'false') . ', items count: ' . count($items)); } $_SESSION['success'] = 'Pago registrado exitosamente'; } else { $_SESSION['error'] = 'Error al registrar el pago. Revise los logs del servidor para más detalles.'; error_log('AdminController: Payment creation failed for user_id: ' . $paymentData['user_id']); } } catch (Exception $e) { $_SESSION['error'] = 'Error interno del servidor al registrar el pago: ' . $e->getMessage(); error_log('AdminController: Payment creation exception: ' . $e->getMessage()); error_log('Payment data: ' . print_r($paymentData, true)); } break; case 'update_payment': $comprobanteFile = $_FILES['comprobante'] ?? null; $comprobanteName = $_POST['comprobante_actual'] ?? null; // conservar por defecto $comprobanteError = null; $oldComprobante = $_POST['comprobante_actual'] ?? null; if ($comprobanteFile && $comprobanteFile['error'] === UPLOAD_ERR_OK && $comprobanteFile['size'] > 0) { $ext = pathinfo($comprobanteFile['name'], PATHINFO_EXTENSION); $allowed = ['jpg','jpeg','png','pdf','webp']; $maxSize = 5 * 1024 * 1024; // 5MB if (!in_array(strtolower($ext), $allowed)) { $comprobanteError = 'Tipo de archivo no permitido.'; } elseif ($comprobanteFile['size'] > $maxSize) { $comprobanteError = 'El archivo excede el tamaño máximo de 5MB.'; } else { // Generar nuevo nombre y mover $newName = 'comprobante_' . time() . '_' . rand(1000,9999) . '.' . $ext; $dest = __DIR__ . '/../../uploads/pagos/' . $newName; if (move_uploaded_file($comprobanteFile['tmp_name'], $dest)) { $comprobanteName = $newName; // Eliminar archivo anterior si existía y fue reemplazado if (!empty($oldComprobante)) { $oldPath = Helper::asset('uploads/pagos/' . $oldComprobante); // Intento mediante ruta pública Helper::deleteFileIfExists($oldPath); // Fallback mediante ruta de sistema $oldFsPath = __DIR__ . '/../../uploads/pagos/' . $oldComprobante; if (file_exists($oldFsPath) && is_file($oldFsPath)) { @unlink($oldFsPath); } } } } } if ($comprobanteError) { $_SESSION['error'] = $comprobanteError; header('Location: ' . Helper::appUrl('admin/pagos')); exit; } // Modo Nota de Cobro (update) $isNota = (isset($_POST['is_nota_cobro']) && $_POST['is_nota_cobro'] == '1'); $paymentMethod = $isNota ? 'Nota de Cobro' : ($_POST['payment_method'] ?? ''); // Permitir elegir estado en Nota y usar 'Emitida' si no viene $status = $isNota ? ($_POST['status'] ?? 'Emitida') : ($_POST['status'] ?? 'Pendiente'); $payment_date = $_POST['payment_date'] ?: ($isNota ? date('Y-m-d') : null); $amount = isset($_POST['amount']) ? (float)$_POST['amount'] : 0; $emprendimiento_id = !empty($_POST['emprendimiento_id']) ? (int)$_POST['emprendimiento_id'] : null; // Items $items = []; $itemsSum = 0; if ($isNota) { $raw = $_POST['nota_items_json'] ?? '[]'; $parsed = json_decode($raw, true); if (is_array($parsed)) { foreach ($parsed as $it) { $desc = trim($it['description'] ?? ''); $qty = (float)($it['quantity'] ?? 0); $price = (float)($it['price'] ?? 0); $sub = (float)($it['subtotal'] ?? ($qty * $price)); if ($desc === '' || $qty <= 0) continue; $items[] = [ 'description' => $desc, 'quantity' => $qty, 'price' => $price, 'subtotal' => $sub, ]; $itemsSum += $sub; } } } $subtotal = $isNota ? ( ($itemsSum > 0) ? $itemsSum : $amount ) : ($amount); // Datos adicionales de la Nota $consecutive = trim($_POST['consecutive'] ?? '') ?: null; $issue_city = trim($_POST['issue_city'] ?? '') ?: null; $issue_date = $_POST['issue_date'] ?? null; $due_date = $_POST['due_date'] ?? null; $service_start_date = $_POST['service_start_date'] ?? null; $service_end_date = $_POST['service_end_date'] ?? null; $service_city = trim($_POST['service_city'] ?? '') ?: null; $iva_percent = isset($_POST['iva_percent']) ? (float)$_POST['iva_percent'] : 0.0; $retefuente_percent = isset($_POST['retefuente_percent']) ? (float)$_POST['retefuente_percent'] : 0.0; $ica_percent = isset($_POST['ica_percent']) ? (float)$_POST['ica_percent'] : 0.0; // Calcular montos de impuestos/retenciones con redondeo entero $iva_amount = isset($_POST['iva_amount']) ? (float)$_POST['iva_amount'] : round($subtotal * $iva_percent / 100); $retefuente_amount = isset($_POST['retefuente_amount']) ? (float)$_POST['retefuente_amount'] : round($subtotal * $retefuente_percent / 100); $ica_amount = isset($_POST['ica_amount']) ? (float)$_POST['ica_amount'] : round($subtotal * $ica_percent / 100); $total = $subtotal + $iva_amount - $retefuente_amount - $ica_amount; if ($isNota) { $amount = $total; } // DOCX deshabilitado: vista previa 100% digital $notaDocName = null; $paymentData = [ 'user_id' => $_POST['user_id'], 'amount' => $amount, 'payment_method' => $paymentMethod, 'status' => $status, 'is_nota_cobro' => $isNota ? 1 : 0, 'subtotal' => $subtotal, 'total' => $total, ]; // Solo incluir campos que tienen valor para no sobrescribir con vacíos if (isset($_POST['reference']) && $_POST['reference'] !== '') { $paymentData['reference'] = $_POST['reference']; } if (isset($_POST['description']) && $_POST['description'] !== '') { $paymentData['description'] = $_POST['description']; } if ($payment_date !== null && $payment_date !== '') { $paymentData['payment_date'] = $payment_date; } if ($comprobanteName !== null) { $paymentData['comprobante'] = $comprobanteName; } if ($notaDocName !== null) { $paymentData['nota_cobro_doc'] = $notaDocName; } if ($emprendimiento_id !== null) { $paymentData['emprendimiento_id'] = $emprendimiento_id; } if ($consecutive !== null && $consecutive !== '') { $paymentData['consecutive'] = $consecutive; } if ($issue_city !== null && $issue_city !== '') { $paymentData['issue_city'] = $issue_city; } if (!empty($issue_date)) { $paymentData['issue_date'] = $issue_date; } if (!empty($due_date)) { $paymentData['due_date'] = $due_date; } if (!empty($service_start_date)) { $paymentData['service_start_date'] = $service_start_date; } if (!empty($service_end_date)) { $paymentData['service_end_date'] = $service_end_date; } if ($service_city !== null && $service_city !== '') { $paymentData['service_city'] = $service_city; } if (isset($iva_percent)) { $paymentData['iva_percent'] = $iva_percent; } if (isset($iva_amount)) { $paymentData['iva_amount'] = $iva_amount; } if (isset($retefuente_percent)) { $paymentData['retefuente_percent'] = $retefuente_percent; } if (isset($retefuente_amount)) { $paymentData['retefuente_amount'] = $retefuente_amount; } if (isset($ica_percent)) { $paymentData['ica_percent'] = $ica_percent; } if (isset($ica_amount)) { $paymentData['ica_amount'] = $ica_amount; } $result = $paymentModel->update($_POST['payment_id'], $paymentData); // Guardar detalles if ($result && $isNota) { $pd = new PaymentDetail(); $pd->replaceForPayment((int)$_POST['payment_id'], $items); } if ($result) { $_SESSION['success'] = 'Pago actualizado exitosamente'; } else { $_SESSION['error'] = 'Error al actualizar el pago'; } break; case 'delete_payment': // Eliminar archivo de comprobante si existe $payment = $paymentModel->getById($_POST['payment_id']); if ($payment && !empty($payment['comprobante'])) { $path = Helper::asset('uploads/pagos/' . $payment['comprobante']); Helper::deleteFileIfExists($path); $fsPath = __DIR__ . '/../../uploads/pagos/' . $payment['comprobante']; if (file_exists($fsPath) && is_file($fsPath)) { @unlink($fsPath); } } $result = $paymentModel->delete($_POST['payment_id']); if ($result) { $_SESSION['success'] = 'Pago eliminado exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar el pago'; } break; } header('Location: ' . Helper::appUrl('admin/pagos')); exit; } $content = $this->renderViewToString('admin/pagos', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function getPayment() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/Payment.php'; require_once __DIR__ . '/../models/PaymentDetail.php'; $paymentModel = new Payment(); $payment = $paymentModel->getById($_GET['id']); if ($payment) { $pd = new PaymentDetail(); $payment['details'] = $pd->getByPaymentId((int)$_GET['id']); } if ($payment) { echo json_encode(['success' => true, 'payment' => $payment]); } else { echo json_encode(['success' => false, 'message' => 'Pago no encontrado']); } } // Listado básico de productos (para autocompletar en Nota de Cobro) public function getProductsBasic() { require_once __DIR__ . '/../models/Product.php'; $prodModel = new Product(); $rows = $prodModel->getAll(); $products = []; foreach ($rows as $r) { $nombre = $r['producto'] ?? ''; $precio = isset($r['precio']) ? (float)$r['precio'] : 0; // Horas puede venir del LEFT JOIN (alias horas) o de la columna base $horas = isset($r['horas']) ? (float)$r['horas'] : 0; $precioHora = 0; if ($horas > 0) { $precioHora = round($precio / $horas); } else if ($precio > 0) { // Si no hay horas, usamos el precio como precio/hora por defecto $precioHora = round($precio); } $products[] = [ 'id' => (int)($r['id'] ?? 0), 'producto' => $nombre, 'precio' => (float)$precio, 'horas' => (float)$horas, 'precio_hora' => (int)$precioHora, ]; } echo json_encode(['success' => true, 'products' => $products]); } // Vista previa de Nota de Cobro en modal public function notaCobroPreview() { header('Content-Type: application/json; charset=utf-8'); if (!isset($_GET['id'])) { echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } $id = (int)$_GET['id']; require_once __DIR__ . '/../models/Payment.php'; require_once __DIR__ . '/../models/PaymentDetail.php'; require_once __DIR__ . '/../models/Emprendimiento.php'; require_once __DIR__ . '/../models/Settings.php'; require_once __DIR__ . '/../models/User.php'; $paymentModel = new Payment(); $detailModel = new PaymentDetail(); $emprModel = new Emprendimiento(); $settings = new Settings(); $p = $paymentModel->getById($id); if (!$p) { echo json_encode(['success' => false, 'message' => 'Pago no encontrado']); return; } $items = $detailModel->getByPaymentId($id); $empr = null; if (!empty($p['emprendimiento_id'])) { $empr = $emprModel->findById((int)$p['emprendimiento_id']); } // Datos del prestador desde Settings $provider = $settings->getJson('provider_info', [ 'name' => '', 'doc_type' => '', 'doc_num' => '', 'nit_rut' => '', 'phone' => '', 'email' => '', 'address' => '', 'city' => '', 'dept' => '', 'logo' => '' ]); $accounts = $settings->getJson('bank_accounts', []); // Si no hay configuración previa y el usuario ya nos autorizó, sembramos valores por defecto $shouldSeedProvider = empty($provider['name']); $shouldSeedAccounts = empty($accounts); if ($shouldSeedProvider) { $provider = [ 'name' => 'Nevin Cristopher Paredes Rojas', 'doc_type' => 'PPT', 'doc_num' => '6982489', 'nit_rut' => '700365064', 'phone' => '3028424064', 'email' => 'nevin@emprendo.com.co', 'address' => 'Cl. 17 entre avenidas 5ta y 6ta. #5A-32, La Cabrera, Cúcuta', 'city' => 'Cúcuta', 'dept' => 'Norte de Santander', 'logo' => Helper::asset('assets/img/imagotipo.webp'), ]; $settings->setJson('provider_info', $provider); } if ($shouldSeedAccounts) { $accounts = [ [ 'method' => 'Por consignación o transferencia', 'account_type' => 'Ahorros', 'bank' => 'Bancolombia', 'account_number' => '81662605204', 'holder' => 'Nevin Cristopher Paredes Rojas', ], [ 'method' => 'Por consignación o transferencia', 'account_type' => 'Ahorros', 'bank' => 'Nequi', 'account_number' => '3028424064', 'holder' => 'Nevin Cristopher Paredes Rojas', ], ]; $settings->setJson('bank_accounts', $accounts); } // Logo del prestador (URL absoluto o relativo). Fallback a imagen por defecto. $logoUrl = ''; if (is_array($provider) && !empty($provider['logo'])) { $logoUrl = $provider['logo']; } if (!$logoUrl) { $logoUrl = Helper::asset('assets/img/imagotipo.webp'); } // Resolver firma del prestador a partir del documento configurado $providerSignatureSrc = null; try { $uProvModel = new User(); $provDocNum = isset($provider['doc_num']) ? trim((string)$provider['doc_num']) : ''; if ($provDocNum !== '') { $provUser = $uProvModel->findByDocumento($provDocNum); if (is_array($provUser) && !empty($provUser)) { if (!empty($provUser['agreement_signature_blob'])) { $providerSignatureSrc = 'data:image/png;base64,' . base64_encode($provUser['agreement_signature_blob']); } elseif (!empty($provUser['agreement_signature'])) { $providerSignatureSrc = $provUser['agreement_signature']; } if (!$providerSignatureSrc && !empty($provUser['id'])) { $ag = $uProvModel->getAgreementData((int)$provUser['id']); if (!empty($ag['signature_blob'])) { $providerSignatureSrc = 'data:image/png;base64,' . base64_encode($ag['signature_blob']); } elseif (!empty($ag['signature_path'])) { $providerSignatureSrc = $ag['signature_path']; } } } } // Último fallback: usar la firma del admin logueado si existe if (!$providerSignatureSrc && !empty($_SESSION['user_id'])) { $agAdmin = $uProvModel->getAgreementData((int)$_SESSION['user_id']); if (!empty($agAdmin['signature_blob'])) { $providerSignatureSrc = 'data:image/png;base64,' . base64_encode($agAdmin['signature_blob']); } elseif (!empty($agAdmin['signature_path'])) { $providerSignatureSrc = $agAdmin['signature_path']; } } } catch (Throwable $e) { /* noop */ } // Formateadores $fmt = function($n) { return '$' . number_format((float)$n, 0, ',', '.'); }; $dateHuman = function($iso) { if (!$iso) return ''; $ts = strtotime($iso); if (!$ts) return ''; $meses = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre']; $d = (int)date('d', $ts); $m = $meses[(int)date('m', $ts)-1] ?? date('m', $ts); $y = date('Y', $ts); return $d . ' de ' . $m . ' de ' . $y; }; $issue_city = $p['issue_city'] ?: ($provider['city'] ?: ''); $issue_date = $p['issue_date'] ?: ($p['payment_date'] ?: date('Y-m-d')); // Sexta línea: solo la fecha en formato humano $header = $dateHuman($issue_date); // Contacto del prestador para encabezado derecho (líneas 1 a 5) $line1 = ''; $addr = trim((string)($provider['address'] ?? '')); if ($addr !== '') { $parts = array_map('trim', explode(',', $addr)); // Tomar primera parte y, si existe, segunda (sin ciudad) $line1 = $parts[0] ?? ''; if (isset($parts[1])) { $maybeSector = $parts[1]; $cityLower = strtolower(trim((string)($provider['city'] ?? ''))); if ($maybeSector !== '' && strtolower($maybeSector) !== $cityLower) { $line1 = $line1 ? ($line1 . ', ' . $maybeSector) : $maybeSector; } } if ($line1 === '') { $line1 = $addr; } } $line2 = trim(($provider['city'] ?? '') . ((($provider['dept'] ?? '') !== '') ? ' - ' . $provider['dept'] : '')); $line3 = trim((string)($provider['email'] ?? '')); // Formato especial solicitado para el teléfono: (+58) 302 842 40 64 $line4 = ''; $digits = preg_replace('/\D+/', '', (string)($provider['phone'] ?? '')); $telHref = ''; if (strlen($digits) >= 10) { $last10 = substr($digits, -10); $g1 = substr($last10, 0, 3); $g2 = substr($last10, 3, 3); $g3 = substr($last10, 6, 2); $g4 = substr($last10, 8, 2); $line4 = '(+58) ' . $g1 . ' ' . $g2 . ' ' . $g3 . ' ' . $g4; $telHref = '+58' . $last10; } else if ($digits !== '') { $line4 = '(+58) ' . $digits; $telHref = '+58' . $digits; } // Construir HTML con enlaces clicables (pantalla) y limpio en impresión mediante CSS global de la vista $linesHtml = []; if ($line1 !== '') $linesHtml[] = '<div>' . htmlspecialchars($line1) . '</div>'; if ($line2 !== '') $linesHtml[] = '<div>' . htmlspecialchars($line2) . '</div>'; if ($line3 !== '') { $emailDisp = htmlspecialchars($line3); $emailHref = 'mailto:' . rawurlencode($line3); $linesHtml[] = '<div><a href="' . $emailHref . '">' . $emailDisp . '</a></div>'; } if ($line4 !== '') { $telDisp = htmlspecialchars($line4); if ($telHref !== '') { $linesHtml[] = '<div><a href="tel:' . htmlspecialchars($telHref) . '">' . $telDisp . '</a></div>'; } else { $linesHtml[] = '<div>' . $telDisp . '</div>'; } } // Quinta línea vacía $linesHtml[] = '<div> </div>'; $contactHtml = implode('', $linesHtml); // Destinatario (Cliente/Emprendimiento) $clienteNombre = $empr['razon_social'] ?? ($empr['nombre_comercial'] ?? ($p['user_name'] ?? 'Cliente')); $clienteCiudad = $empr['localidad'] ?? ''; $clienteDepto = $empr['pais'] ?? ''; // Documento del destinatario: // - Si hay emprendimiento: usar su "documento". // - Si NO hay emprendimiento: usar primero el documento del usuario (users.documento); // si está vacío, como fallback intentar con algún emprendimiento del usuario. $clienteDoc = ''; if (is_array($empr) && !empty($empr['documento'])) { $clienteDoc = (string)$empr['documento']; } else { // Intentar con el documento del usuario try { require_once __DIR__ . '/../models/User.php'; $uModel = new User(); $uRow = $uModel->findById((int)($p['user_id'] ?? 0)); if (is_array($uRow) && !empty($uRow['documento'])) { $clienteDoc = (string)$uRow['documento']; } } catch (Throwable $e) { /* noop */ } // Fallback: si sigue vacío, buscar algún emprendimiento del usuario if ($clienteDoc === '') { try { $emprsUser = $emprModel->getByUser((int)($p['user_id'] ?? 0)); if (is_array($emprsUser) && !empty($emprsUser)) { foreach ($emprsUser as $eRow) { if (!empty($eRow['documento'])) { $clienteDoc = (string)$eRow['documento']; break; } } } } catch (Throwable $e) { /* noop */ } } } // Tabla de ítems $rows = ''; foreach ($items as $it) { $rows .= '<tr>' . '<td>' . htmlspecialchars($it['description']) . '</td>' . '<td class="text-end">' . htmlspecialchars(rtrim(rtrim(number_format((float)$it['quantity'], 2, ',', '.'), '0'), ',')) . '</td>' . '<td class="text-end">' . $fmt($it['price']) . '</td>' . '<td class="text-end">' . $fmt($it['subtotal']) . '</td>' . '</tr>'; } if ($rows === '') { $rows = '<tr><td colspan="4" class="text-center text-muted">Sin ítems</td></tr>'; } // Impuestos/retenciones $ivaAmt = $p['iva_amount'] ?? 0; $rtfAmt = $p['retefuente_amount'] ?? 0; $icaAmt = $p['ica_amount'] ?? 0; $ivaPct = $p['iva_percent'] ?? 0; $rtfPct = $p['retefuente_percent'] ?? 0; $icaPct = $p['ica_percent'] ?? 0; // Render HTML ob_start(); ?> <section class="mb-3"> <div class="fw-bold">Señores</div> <div><?= htmlspecialchars($clienteNombre) ?></div> <?php if (!empty($clienteDoc)): ?><div><?= htmlspecialchars($clienteDoc) ?></div><?php endif; ?> <div><?= htmlspecialchars(trim($clienteCiudad . ($clienteDepto ? ' - ' . $clienteDepto : ''))) ?></div> </section> <section class="mb-3"> Yo, <?= htmlspecialchars($provider['name'] ?: 'Prestador') ?>, identificado con <?= htmlspecialchars($provider['doc_type'] ?: 'Doc') ?> No. <?= htmlspecialchars($provider['doc_num'] ?: '-') ?><?= !empty($provider['nit_rut']) ? ', NIT/RUT ' . htmlspecialchars($provider['nit_rut']) : '' ?>, me permito presentar la siguiente cuenta de cobro por concepto de <?= htmlspecialchars($p['description'] ?: 'Servicio') ?><?php if ($p['service_start_date'] || $p['service_end_date']): ?>, realizado entre el <?= htmlspecialchars(date('d/m/Y', strtotime($p['service_start_date'] ?: $issue_date))) ?> y el <?= htmlspecialchars(date('d/m/Y', strtotime($p['service_end_date'] ?: $issue_date))) ?><?php endif; ?><?= $p['service_city'] ? ' en la ciudad de ' . htmlspecialchars($p['service_city']) : '' ?>. </section> <section class="mb-3"> <div class="fw-bold">CONCEPTO</div> <div><?= htmlspecialchars($p['description'] ?: 'Servicio') ?></div> <div class="table-responsive mt-2"> <table class="table table-sm align-middle"> <thead class="table-light"> <tr> <th>Descripción</th> <th class="text-end">Cantidad</th> <th class="text-end">Precio/Hora</th> <th class="text-end">Subtotal</th> </tr> </thead> <tbody><?= $rows ?></tbody> </table> </div> </section> <section class="mb-3"> <div class="d-flex justify-content-end gap-4 flex-wrap"> <div class="text-end"><div class="text-muted small">Subtotal</div><div class="fw-bold"><?= $fmt($p['subtotal'] ?? 0) ?> COP</div></div> <div class="text-end"><div class="text-muted small">IVA<?= $ivaPct ? ' (' . (float)$ivaPct . '%)' : '' ?></div><div class="fw-bold"><?= $fmt($ivaAmt) ?></div></div> <div class="text-end"><div class="text-muted small">ReteFuente<?= $rtfPct ? ' (' . (float)$rtfPct . '%)' : '' ?></div><div class="fw-bold">-<?= $fmt($rtfAmt) ?></div></div> <div class="text-end"><div class="text-muted small">ICA<?= $icaPct ? ' (' . (float)$icaPct . '%)' : '' ?></div><div class="fw-bold">-<?= $fmt($icaAmt) ?></div></div> <div class="text-end"><div class="text-muted small">TOTAL</div><div class="fw-bold fs-5"><?= $fmt($p['total'] ?? 0) ?> COP</div></div> </div> </section> <!-- Datos del prestador presentados en el encabezado derecho --> <?php if (!empty($accounts) && is_array($accounts)): ?> <section class="mb-3"> <div class="fw-bold">FORMA DE PAGO</div> <div class="table-responsive mt-2"> <table class="table table-sm align-middle"> <thead class="table-light"><tr><th>Forma de pago</th><th>Tipo de cuenta</th><th>Entidad bancaria</th><th>Número de cuenta</th><th>A nombre de</th></tr></thead> <tbody> <?php foreach ($accounts as $acc): ?> <tr> <td><?= htmlspecialchars($acc['method'] ?? 'Transferencia') ?></td> <td><?= htmlspecialchars($acc['account_type'] ?? '') ?></td> <td><?= htmlspecialchars($acc['bank'] ?? '') ?></td> <td><?= htmlspecialchars($acc['account_number'] ?? '') ?></td> <td><?= htmlspecialchars($acc['holder'] ?? ($provider['name'] ?? '')) ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </section> <?php endif; ?> <section class="mb-4" style="white-space: pre-wrap;"> Declaro bajo la gravedad de juramento que los servicios aquí descritos fueron prestados efectivamente, y que los valores cobrados corresponden a lo pactado con la entidad. </section> <section class="pt-5 text-center"> <?php if (!empty($providerSignatureSrc)): ?> <div><img src="<?= $providerSignatureSrc ?>" alt="Firma" style="max-height:80px;max-width:360px;object-fit:contain;opacity:.95;"></div> <?php else: ?> <div style="height: 60px;"></div> <div>______________________________</div> <?php endif; ?> <div><?= htmlspecialchars($provider['name'] ?: '') ?></div> <div><?= htmlspecialchars(($provider['doc_type'] ?: '')) ?> <?= htmlspecialchars(($provider['doc_num'] ?: '')) ?></div> </section> <?php $html = ob_get_clean(); echo json_encode(['success' => true, 'html' => $html, 'header' => $header, 'logo' => $logoUrl, 'contact' => $contactHtml]); } // Siguiente consecutivo para Nota de Cobro (JSON) public function nextConsecutive() { header('Content-Type: application/json; charset=utf-8'); require_once __DIR__ . '/../models/Payment.php'; $pm = new Payment(); $code = $pm->getNextConsecutive('NC'); echo json_encode(['success' => true, 'code' => $code]); } // Ajustes del sistema: datos del prestador y cuentas bancarias (UI) public function ajustes() { require_once __DIR__ . '/../models/Settings.php'; $settings = new Settings(); $data = $this->getUserData(); $data['title'] = 'Ajustes del Sistema'; $data['provider'] = $settings->getJson('provider_info', [ 'name' => '', 'doc_type' => '', 'doc_num' => '', 'nit_rut' => '', 'phone' => '', 'email' => '', 'address' => '', 'city' => '', 'dept' => '', 'logo' => '' ]); $data['accounts'] = $settings->getJson('bank_accounts', []); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $prov = [ 'name' => trim($_POST['name'] ?? ''), 'doc_type' => trim($_POST['doc_type'] ?? ''), 'doc_num' => trim($_POST['doc_num'] ?? ''), 'nit_rut' => trim($_POST['nit_rut'] ?? ''), 'phone' => trim($_POST['phone'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'address' => trim($_POST['address'] ?? ''), 'city' => trim($_POST['city'] ?? ''), 'dept' => trim($_POST['dept'] ?? ''), 'logo' => trim($_POST['logo'] ?? ''), ]; $settings->setJson('provider_info', $prov); $accounts = json_decode($_POST['accounts_json'] ?? '[]', true); if (!is_array($accounts)) { $accounts = []; } $settings->setJson('bank_accounts', $accounts); $_SESSION['success'] = 'Ajustes guardados correctamente'; header('Location: ' . Helper::appUrl('admin/ajustes')); exit; } $content = $this->renderViewToString('admin/ajustes', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } // Generador simple de DOCX por placeholders private function generateNotaCobroDoc($templateAbs, $destAbs, $placeholders) { // Copiar plantilla primero if (!@copy($templateAbs, $destAbs)) { return false; } // Intentar abrir como zip y reemplazar document.xml if (class_exists('ZipArchive')) { $zip = new ZipArchive(); if ($zip->open($destAbs) === true) { $xml = $zip->getFromName('word/document.xml'); if ($xml !== false) { foreach ($placeholders as $k => $v) { $xml = str_replace($k, htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), $xml); } $zip->addFromString('word/document.xml', $xml); } $zip->close(); return true; } } return true; // al menos quedó copia } public function frases() { require_once __DIR__ . '/../models/Quote.php'; $quoteModel = new Quote(); $data = $this->getUserData(); $data['quotes'] = $quoteModel->getAll(); $data['title'] = 'Frases Motivacionales'; // Manejar acciones CRUD if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_quote': $frase = trim($_POST['frase'] ?? ''); $autor = trim($_POST['autor'] ?? ''); if ($frase === '') { $_SESSION['error'] = 'La frase es requerida'; break; } $result = $quoteModel->create($frase, $autor); if ($result) { $_SESSION['success'] = 'Frase agregada exitosamente'; } else { $_SESSION['error'] = 'Error al agregar la frase'; } break; case 'delete_quote': $result = $quoteModel->delete($_POST['quote_id']); if ($result) { $_SESSION['success'] = 'Frase eliminada exitosamente'; } else { $_SESSION['error'] = 'Error al eliminar la frase'; } break; } header('Location: ' . Helper::appUrl('admin/frases')); exit; } $content = $this->renderViewToString('admin/frases', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function updateProfile() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Método no permitido']); return; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $user_id = $_SESSION['user_id']; $name = $_POST['name'] ?? ''; $email = $_POST['email'] ?? ''; $profile_image = null; // Validar campos requeridos if (empty($name) || empty($email)) { echo json_encode(['success' => false, 'message' => 'Nombre y email son requeridos']); return; } // Validar formato de email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['success' => false, 'message' => 'Formato de email inválido']); return; } // Manejar subida de imagen de perfil if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) { $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $file_type = $_FILES['profile_image']['type']; if (!in_array($file_type, $allowed_types)) { echo json_encode(['success' => false, 'message' => 'Tipo de archivo no permitido. Solo se permiten imágenes (JPG, PNG, GIF, WEBP)']); return; } $max_size = 5 * 1024 * 1024; // 5MB if ($_FILES['profile_image']['size'] > $max_size) { echo json_encode(['success' => false, 'message' => 'El archivo es demasiado grande. Máximo 5MB']); return; } $ext = pathinfo($_FILES['profile_image']['name'], PATHINFO_EXTENSION); $profile_dir = __DIR__ . '/../../uploads/profile_images/'; if (!is_dir($profile_dir)) { mkdir($profile_dir, 0777, true); } $profile_image_name = 'user_' . $user_id . '_' . time() . '.' . $ext; $profile_path = $profile_dir . $profile_image_name; if (move_uploaded_file($_FILES['profile_image']['tmp_name'], $profile_path)) { $profile_image = Helper::asset('uploads/profile_images/' . $profile_image_name); } else { echo json_encode(['success' => false, 'message' => 'Error al subir la imagen']); return; } } // Actualizar perfil en la base de datos (usa versión extendida que elimina la imagen anterior) if ($userModel->updateProfileFull($user_id, $name, $email, $_POST['telefono'] ?? null, $profile_image, null)) { // Actualizar datos de sesión $_SESSION['user_name'] = $name; $_SESSION['user_email'] = $email; if ($profile_image) { $_SESSION['profile_image'] = $profile_image; } echo json_encode([ 'success' => true, 'message' => 'Perfil actualizado exitosamente', 'data' => [ 'name' => $name, 'email' => $email, 'profile_image' => $profile_image ] ]); } else { echo json_encode(['success' => false, 'message' => 'El email ya está registrado por otro usuario']); } } public function getUser() { if (!isset($_GET['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no proporcionado']); return; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $user = $userModel->findById($_GET['id']); if ($user) { echo json_encode(['success' => true, 'user' => $user]); } else { echo json_encode(['success' => false, 'message' => 'Usuario no encontrado']); } } public function bienvenida() { require_once '../cuentame/models/User.php'; require_once '../cuentame/models/Quote.php'; // Aquí deberías cargar las conversaciones reales si tienes un modelo para ello $userModel = new User(); $user = $userModel->findById($_SESSION['user_id']); $data = [ 'user_name' => $_SESSION['user_name'] ?? ($user['name'] ?? 'Admin'), 'profile_image' => $user['profile_image'] ?? Helper::asset('assets/img/user-default.png'), 'user_email' => $user['email'] ?? '', 'title' => 'Bienvenida', 'conversations' => [], // Aquí deberías cargar las conversaciones reales ]; $content = $this->renderViewToString('admin/bienvenida', $data); $this->renderLayout('admin_layout', $data + ['content' => $content]); } public function getProductsByLinea() { header('Content-Type: application/json; charset=utf-8'); if (!isset($_GET['linea'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Línea no proporcionada']); return; } require_once __DIR__ . '/../models/Product.php'; $productModel = new Product(); $linea = trim($_GET['linea']); $products = $productModel->getByLinea($linea); echo json_encode(['success' => true, 'products' => $products]); } // Obtener usuarios con rol admin o manager para dropdown de especialistas public function getSpecialists() { header('Content-Type: application/json'); try { require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $users = $userModel->getAdminsAndManagers(); echo json_encode(['success' => true, 'users' => $users]); } catch (Throwable $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error interno']); } } // Puedes agregar más métodos para otras secciones // Helpers para renderizar vistas y layouts private function renderViewToString($view, $data = []) { extract($data); ob_start(); require __DIR__ . '/../views/' . $view . '.php'; return ob_get_clean(); } private function renderLayout($layout, $data = []) { if (isset($data['content'])) { $content = $data['content']; unset($data['content']); } extract($data); require __DIR__ . '/../views/layouts/' . $layout . '.php'; } }
Coded With 💗 by
0x6ick