Tul xxx Tul
User / IP
:
216.73.216.183
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: ClientesController.php
<?php require_once '../cuentame/core/Controller.php'; require_once __DIR__ . '/../core/Helper.php'; class ClientesController extends Controller { public function __construct() { session_start(); 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('admin/dashboard')); exit; } // Permitir roles 'emprendedor' y 'cliente' $roleNorm = strtolower(trim((string)$role)); if (!in_array($roleNorm, ['emprendedor', 'cliente'])) { header('Location: ' . Helper::appUrl('login')); exit; } } // Helpers para renderizar vistas con layout 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'; } 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'] ?? 'Emprendedor'), 'user_email' => $user['email'] ?? '', 'profile_image' => $user['profile_image'] ?? Helper::asset('assets/img/user-default.png'), 'company_logo' => $user['company_logo'] ?? Helper::asset('assets/img/logo-blanco.png'), 'title' => 'Panel de Clientes' ]; } public function index() { $data = $this->getUserData(); // Datos del dashboard (igual que el panel) require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/Product.php'; require_once __DIR__ . '/../models/ProjectAction.php'; require_once __DIR__ . '/../models/Transaction.php'; require_once __DIR__ . '/../models/Payment.php'; require_once __DIR__ . '/../models/Activity.php'; require_once __DIR__ . '/../models/User.php'; $userId = $_SESSION['user_id']; $projectModel = new Project(); $productModel = new Product(); $paTotalsModel = new ProjectAction(); $transactionModel = new Transaction(); $paymentModel = new Payment(); $activityModel = new Activity(); $userModel = new User(); $data['projects'] = $projectModel->getByUser($userId); // KPIs alineados con Admin (mini-dashboard de usuarios) try { // 1) Conteos de proyectos $activeStatuses = ['En Progreso']; $completedStatuses = ['Logrado']; $actives = 0; $completed = 0; foreach ($data['projects'] as $proj) { $st = $proj['status'] ?? ''; if (in_array($st, $activeStatuses, true)) { $actives++; } if (in_array($st, $completedStatuses, true)) { $completed++; } } $data['kpi_active_projects'] = $actives; $data['kpi_completed_projects'] = $completed; // 2) Proyectos en Progreso (mismo conteo que activos) $data['kpi_upcoming_activities'] = $actives; // 3) Presupuestado: suma de precio de acciones Pendiente o En Progreso $data['kpi_budgeted_amount'] = (int)$paTotalsModel->getSumPrecioPendienteEnProgresoByUser($userId); // 4) Pendiente por pagar: Logradas - Abonos a proyectos $sumLograda = (int)$paTotalsModel->getSumPrecioLogradaByUser($userId); $sumAbonos = (int)$paymentModel->getTotalPaidToProjects($userId); $due = $sumLograda - $sumAbonos; if ($due < 0) { $due = 0; } $data['kpi_due_to_pay'] = (int)$due; $data['kpi_due_actions_sum'] = (int)$sumLograda; $data['kpi_due_paid_sum'] = (int)$sumAbonos; } catch (Throwable $e) { $data['kpi_active_projects'] = 0; $data['kpi_completed_projects'] = 0; $data['kpi_upcoming_activities'] = 0; $data['kpi_budgeted_amount'] = 0; $data['kpi_due_to_pay'] = 0; $data['kpi_due_actions_sum'] = 0; $data['kpi_due_paid_sum'] = 0; } $data['transactions'] = $transactionModel->getByUser($userId); $data['payments'] = $paymentModel->getByUser($userId); // Saldo Disponible: pagos Recibidos - Abonos a proyectos (con desglose) try { $received = (int)$paymentModel->getTotalByUser($userId) ?: 0; $paidToProj = (int)$paymentModel->getTotalPaidToProjects($userId) ?: 0; $avail = $received - $paidToProj; if ($avail < 0) { $avail = 0; } $data['available_balance'] = (int)$avail; $data['kpi_balance_received_sum'] = (int)$received; $data['kpi_balance_paid_sum'] = (int)$paidToProj; } catch (Throwable $e) { /* noop */ } // Actividades y estadísticas $data['activities'] = $activityModel->getByUser($userId); $data['today_activities'] = $activityModel->getTodayActivities($userId); $data['week_activities'] = $activityModel->getWeekActivities($userId); $data['month_activities'] = $activityModel->getMonthActivities($userId); $data['activity_stats'] = $activityModel->getStats($userId); // Estadísticas de pagos (mantener, sin sobreescribir available_balance ya calculado) $data['user_stats'] = $paymentModel->getUserStats($userId); // Monto pendiente: suma de (budget - paid_amount) para estados Pendiente/En Progreso/Logrado try { if (method_exists($projectModel, 'getPendingAmountByUser')) { $data['pending_amount'] = (int)$projectModel->getPendingAmountByUser($userId); } else { $data['pending_amount'] = 0; } } catch (Throwable $__) { $data['pending_amount'] = 0; } // Mostrar modal de acuerdo solo si el usuario no lo ha aceptado aún try { $data['agreement_required'] = !$userModel->hasAcceptedAgreement($userId); } catch (Throwable $e) { $data['agreement_required'] = true; } $data['agreement_version'] = '2025-09-12-v1'; $content = $this->renderViewToString('clientes/index', $data); $this->renderLayout('clientes_layout', $data + ['content' => $content]); } public function emprendimientos() { require_once __DIR__ . '/../models/User.php'; require_once __DIR__ . '/../models/Emprendimiento.php'; $userModel = new User(); $emprendimientoModel = new Emprendimiento(); $data = $this->getUserData(); // Solo mostrar emprendimientos del usuario logueado $data['emprendimientos'] = $emprendimientoModel->getByUser($_SESSION['user_id']); $data['title'] = 'Mis eMprendimientos'; // Manejar acciones CRUD (solo para emprendimientos del usuario) if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_emprendimiento': $emprendimientoData = [ 'user_id' => $_SESSION['user_id'], // Forzar user_id del usuario logueado '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'; } else { $_SESSION['error'] = 'Error al crear el emprendimiento'; } break; case 'update_emprendimiento': // Verificar que el emprendimiento pertenece al usuario $emprendimiento = $emprendimientoModel->findById($_POST['emprendimiento_id']); if (!$emprendimiento || $emprendimiento['user_id'] != $_SESSION['user_id']) { $_SESSION['error'] = 'No tienes permisos para editar este emprendimiento'; break; } $emprendimientoData = [ 'user_id' => $_SESSION['user_id'], // Forzar user_id del usuario logueado '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' => 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'; } else { $_SESSION['error'] = 'Error al actualizar el emprendimiento'; } break; case 'delete_emprendimiento': // Verificar que el emprendimiento pertenece al usuario $emprendimiento = $emprendimientoModel->findById($_POST['emprendimiento_id']); if (!$emprendimiento || $emprendimiento['user_id'] != $_SESSION['user_id']) { $_SESSION['error'] = 'No tienes permisos para eliminar este emprendimiento'; break; } $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('clientes/emprendimientos')); exit; } // Reutilizar la vista del admin $content = $this->renderViewToString('clientes/emprendimientos', $data); $this->renderLayout('clientes_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 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']); // Verificar que el emprendimiento pertenece al usuario logueado if (!$emprendimiento || $emprendimiento['user_id'] != $_SESSION['user_id']) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'No tienes permisos para ver este emprendimiento']); return; } if ($emprendimiento) { echo json_encode(['success' => true, 'emprendimiento' => $emprendimiento]); } else { echo json_encode(['success' => false, 'message' => 'Emprendimiento no encontrado']); } } 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; if (empty($name) || empty($email)) { echo json_encode(['success' => false, 'message' => 'Nombre y email son requeridos']); return; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['success' => false, 'message' => 'Formato de email inválido']); return; } 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 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; } } if ($userModel->updateProfileFull($user_id, $name, $email, $_POST['telefono'] ?? null, $profile_image, null)) { $_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 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(); // Solo proyectos del usuario logueado $projects = $projectModel->getByUser($_SESSION['user_id']); // Enriquecer con imagen de producto y métricas como en admin $products = $productModel->getAll(); $productImages = []; $productPrices = []; foreach ($products as $prod) { $productImages[$prod['producto']] = $prod['imagen']; $productPrices[$prod['producto']] = isset($prod['precio']) ? (int)$prod['precio'] : 0; } foreach ($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; 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'; } elseif ($dias >= 4) { $proj['dias_color'] = 'warning'; } else { $proj['dias_color'] = 'danger'; } } else { $proj['dias_restantes'] = null; $proj['dias_color'] = 'secondary'; } 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, acciones (totales y logradas) 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); } catch (Throwable $e) { $proj['phases_count'] = 0; $proj['actions_count'] = 0; $proj['actions_done'] = 0; $proj['total_hours'] = 0; $proj['total_phase_days'] = 0; } } unset($proj); $data['projects'] = $projects; $data['products'] = $products; // Para mantener UI idéntica, proveer estos arrays aunque el cliente solo vea los suyos $data['users'] = [ $userModel->findById($_SESSION['user_id']) ]; $data['admins_and_managers'] = $userModel->getAdminsAndManagers(); $data['emprendimientos'] = $emprendimientoModel->getByUser($_SESSION['user_id']); $data['title'] = 'Mis Proyectos'; // Manejar acciones CRUD limitadas al usuario if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_project': $projectData = [ 'user_id' => $_SESSION['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'] ?? null, // Forzar estado inicial para clientes a "Solicitado" 'status' => 'Solicitado', 'start_date' => $_POST['start_date'], 'end_date' => $_POST['end_date'], // Campo progress removido del formulario en vista clientes: por defecto 0 'progress' => isset($_POST['progress']) ? $_POST['progress'] : 0, 'budget' => $_POST['budget'] ?? null, 'paid_amount' => $_POST['paid_amount'] ?? null ]; $result = $projectModel->create($projectData); $_SESSION[$result ? 'success' : 'error'] = $result ? 'Proyecto creado exitosamente' : 'Error al crear el proyecto'; break; case 'update_project': // Verificar pertenencia del proyecto al usuario $p = $projectModel->getById($_POST['project_id']); if (!$p || $p['user_id'] != $_SESSION['user_id']) { $_SESSION['error'] = 'No tienes permisos para editar este proyecto'; break; } $projectData = [ 'user_id' => $_SESSION['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'] ?? null, // Forzar a mantener el status existente (clientes no pueden cambiar estado) 'status' => $p['status'] ?? 'Solicitado', 'start_date' => $_POST['start_date'], 'end_date' => $_POST['end_date'], // Mantener progress existente si no viene del formulario 'progress' => isset($_POST['progress']) ? $_POST['progress'] : ($p['progress'] ?? 0), 'budget' => $_POST['budget'] ?? null, 'paid_amount' => $_POST['paid_amount'] ?? null ]; $result = $projectModel->update($_POST['project_id'], $projectData); $_SESSION[$result ? 'success' : 'error'] = $result ? 'Proyecto actualizado exitosamente' : 'Error al actualizar el proyecto'; break; case 'delete_project': // Verificar pertenencia del proyecto al usuario $p = $projectModel->getById($_POST['project_id']); if (!$p || $p['user_id'] != $_SESSION['user_id']) { $_SESSION['error'] = 'No tienes permisos para eliminar este proyecto'; break; } $result = $projectModel->delete($_POST['project_id']); $_SESSION[$result ? 'success' : 'error'] = $result ? 'Proyecto eliminado exitosamente' : 'Error al eliminar el proyecto'; break; } header('Location: ' . Helper::appUrl('clientes/proyectos')); exit; } $content = $this->renderViewToString('clientes/proyectos', $data); $this->renderLayout('clientes_layout', $data + ['content' => $content]); } public function plan_accion() { require_once __DIR__ . '/../models/Project.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $data = $this->getUserData(); $data['title'] = 'Plan de Acción'; $projectModel = new Project(); $actionModel = new ProjectAction(); $projects = $projectModel->getByUser($_SESSION['user_id']); foreach ($projects as &$p) { try { $p['actions'] = $actionModel->getByProjectId($p['id']); } catch (Throwable $e) { $p['actions'] = []; } } unset($p); $data['projects'] = $projects; $content = $this->renderViewToString('clientes/plan_accion', $data); $this->renderLayout('clientes_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/Product.php'; require_once __DIR__ . '/../models/ProjectAction.php'; $projectModel = new Project(); $productModel = new Product(); $projectActionModel = new ProjectAction(); $project = $projectModel->getById($_GET['id']); if (!$project || $project['user_id'] != $_SESSION['user_id']) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'No tienes permisos para ver este proyecto']); return; } $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 para que el cliente pueda verlas (solo lectura) $project['actions'] = $projectActionModel->getByProjectId($project['id']); echo json_encode(['success' => true, 'project' => $project]); } 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() { // Siempre devolver los emprendimientos del usuario actual (ignorar parámetro externo) require_once __DIR__ . '/../models/Emprendimiento.php'; $emprendimientoModel = new Emprendimiento(); $emprendimientos = $emprendimientoModel->getByUser($_SESSION['user_id']); echo json_encode(['success' => true, 'emprendimientos' => $emprendimientos]); } public function pagos() { require_once __DIR__ . '/../models/Payment.php'; $paymentModel = new Payment(); $data = $this->getUserData(); $data['title'] = 'Mis Pagos'; $userId = $_SESSION['user_id']; // Manejar creación/eliminación de pagos por el cliente if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'create_payment': // Validaciones básicas $amount = isset($_POST['amount']) ? (float)$_POST['amount'] : 0; $payment_method = trim($_POST['payment_method'] ?? ''); $reference = trim($_POST['reference'] ?? ''); $description = trim($_POST['description'] ?? ''); $payment_date = trim($_POST['payment_date'] ?? ''); if ($amount <= 0 || $payment_method === '') { $_SESSION['error'] = 'Debe ingresar un monto válido y un método de pago.'; header('Location: ' . Helper::appUrl('clientes/pagos')); exit; } if ($payment_date === '') { $payment_date = date('Y-m-d'); } // Manejar comprobante (opcional) $comprobanteName = null; if (isset($_FILES['comprobante']) && $_FILES['comprobante']['error'] !== UPLOAD_ERR_NO_FILE) { $file = $_FILES['comprobante']; if ($file['error'] === UPLOAD_ERR_OK && $file['size'] > 0) { $allowedExts = ['jpg','jpeg','png','webp','pdf']; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $maxSize = 5 * 1024 * 1024; // 5MB if (!in_array($ext, $allowedExts)) { $_SESSION['error'] = 'Formato de comprobante no permitido. Solo JPG, PNG, WEBP o PDF.'; header('Location: ' . Helper::appUrl('clientes/pagos')); exit; } if ($file['size'] > $maxSize) { $_SESSION['error'] = 'El comprobante excede el tamaño máximo de 5MB.'; header('Location: ' . Helper::appUrl('clientes/pagos')); exit; } $uploadDir = __DIR__ . '/../../uploads/pagos/'; if (!is_dir($uploadDir)) { @mkdir($uploadDir, 0777, true); } $comprobanteName = 'comprobante_' . $userId . '_' . time() . '_' . rand(1000,9999) . '.' . $ext; $dest = $uploadDir . $comprobanteName; if (!move_uploaded_file($file['tmp_name'], $dest)) { $_SESSION['error'] = 'No se pudo guardar el comprobante.'; header('Location: ' . Helper::appUrl('clientes/pagos')); exit; } } } $ok = $paymentModel->create([ 'user_id' => $userId, 'amount' => $amount, 'payment_method' => $payment_method, 'reference' => $reference, 'description' => $description, // Siempre pendiente al crear por el cliente; el admin podrá cambiar el estado 'status' => 'Pendiente', 'payment_date' => $payment_date, 'comprobante' => $comprobanteName ]); $_SESSION[$ok ? 'success' : 'error'] = $ok ? 'Pago registrado. Quedó pendiente de revisión.' : 'No se pudo registrar el pago.'; header('Location: ' . Helper::appUrl('clientes/pagos')); exit; } } // Listado de pagos del usuario (ordenar: fecha desc; mismo día: pagos primero y luego notas de cobro) $payments = $paymentModel->getByUser($userId); if (is_array($payments)) { usort($payments, function($a, $b) { $tsA = !empty($a['payment_date']) ? strtotime($a['payment_date']) : (!empty($a['created_at']) ? strtotime($a['created_at']) : 0); $tsB = !empty($b['payment_date']) ? strtotime($b['payment_date']) : (!empty($b['created_at']) ? strtotime($b['created_at']) : 0); $dayA = $tsA ? date('Y-m-d', $tsA) : ''; $dayB = $tsB ? date('Y-m-d', $tsB) : ''; if ($dayA !== $dayB) { // Descendente por día (más reciente primero) return strcmp($dayB, $dayA); } // Mismo día: pagos primero, notas debajo $notaA = ((int)($a['is_nota_cobro'] ?? 0) === 1) || !empty($a['nota_cobro_doc']); $notaB = ((int)($b['is_nota_cobro'] ?? 0) === 1) || !empty($b['nota_cobro_doc']); if ($notaA !== $notaB) { // false (pago) antes que true (nota) return ($notaA ? 1 : 0) <=> ($notaB ? 1 : 0); } // Si ambos son del mismo tipo en el mismo día, ordenar por tiempo descendente return ($tsB <=> $tsA); }); } $data['payments'] = $payments; $content = $this->renderViewToString('clientes/pagos', $data); $this->renderLayout('clientes_layout', $data + ['content' => $content]); } public function acuerdo() { $data = $this->getUserData(); $data['title'] = 'Mi Acuerdo'; $userId = $_SESSION['user_id']; require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $agreement = $userModel->getAgreementData($userId); // Si aún no ha aceptado, redirigir a bienvenida para forzar modal if (empty($agreement['accepted_at'])) { header('Location: ' . Helper::appUrl('clientes')); 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['agreement'] = $agreement; $data['agreement_signature_src'] = $signatureSrc; $data['agreement_photo_src'] = $agreement['photo'] ?? null; $content = $this->renderViewToString('clientes/acuerdo', $data); $this->renderLayout('clientes_layout', $data + ['content' => $content]); } public function acceptAgreement() { header('Content-Type: application/json'); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Método no permitido']); return; } $userId = $_SESSION['user_id'] ?? null; if (!$userId) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'No autenticado']); return; } require_once __DIR__ . '/../models/User.php'; $userModel = new User(); $uploadDir = __DIR__ . '/../../uploads/agreements/'; if (!is_dir($uploadDir)) { @mkdir($uploadDir, 0777, true); } // 1) Foto (archivo) $photoWebPath = null; if (isset($_FILES['agreement_photo']) && $_FILES['agreement_photo']['error'] !== UPLOAD_ERR_NO_FILE) { $file = $_FILES['agreement_photo']; if ($file['error'] !== UPLOAD_ERR_OK || $file['size'] <= 0) { echo json_encode(['success' => false, 'message' => 'Error al subir la foto']); return; } $allowedExts = ['jpg','jpeg','png','webp']; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExts)) { echo json_encode(['success' => false, 'message' => 'Formato de foto no permitido']); return; } $maxSize = 5 * 1024 * 1024; // 5MB if ($file['size'] > $maxSize) { echo json_encode(['success' => false, 'message' => 'La foto excede 5MB']); return; } $photoName = 'photo_' . $userId . '_' . time() . '_' . rand(1000,9999) . '.' . $ext; $dest = $uploadDir . $photoName; if (!move_uploaded_file($file['tmp_name'], $dest)) { echo json_encode(['success' => false, 'message' => 'No se pudo guardar la foto']); return; } $photoWebPath = Helper::asset('uploads/agreements/' . $photoName); } else { echo json_encode(['success' => false, 'message' => 'Debe adjuntar una foto']); return; } // 2) Firma (dataURL PNG) -> guardar como BLOB en BD $signatureData = $_POST['signature_data'] ?? ''; if (!$signatureData || strpos($signatureData, 'data:image/png;base64,') !== 0) { echo json_encode(['success' => false, 'message' => 'Firma inválida']); return; } $raw = base64_decode(substr($signatureData, strlen('data:image/png;base64,'))); if ($raw === false || strlen($raw) < 256) { echo json_encode(['success' => false, 'message' => 'Firma vacía o corrupta']); return; } $version = trim($_POST['agreement_version'] ?? '2025-09-12-v1'); // Guardar aceptación: foto como archivo, firma como BLOB en BD (sin frase) $ok = $userModel->saveAgreementAcceptance((int)$userId, $photoWebPath, null, $version, null, $raw); if ($ok) { // Actualizar automáticamente la foto de perfil con la foto del acuerdo try { $userModel->setProfileImage((int)$userId, $photoWebPath); } catch (Throwable $e) { /* noop */ } } echo json_encode(['success' => (bool)$ok, 'message' => $ok ? 'Acuerdo registrado' : 'No se pudo registrar el acuerdo']); } // Vista previa de Nota de Cobro (Clientes) con verificación de pertenencia 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; } // Verificar pertenencia del pago al usuario actual $currentUserId = (int)($_SESSION['user_id'] ?? 0); if ((int)($p['user_id'] ?? 0) !== $currentUserId) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'No tienes permisos para ver esta Nota de Cobro']); 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, sembrar 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 $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']; } } } } } catch (Throwable $e) { /* noop */ } // Fallback: si no se encuentra firma del proveedor, intentar con la del admin (asumiendo que hay un admin por defecto) if (!$providerSignatureSrc) { $adminUser = $uProvModel->findAdmin(); if ($adminUser && !empty($adminUser['id'])) { $agAdmin = $uProvModel->getAgreementData((int)$adminUser['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']; } } } // 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')); $header = $dateHuman($issue_date); // Contacto del prestador para encabezado derecho $line1 = ''; $addr = trim((string)($provider['address'] ?? '')); if ($addr !== '') { $parts = array_map('trim', explode(',', $addr)); $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'] ?? '')); $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; } $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>'; } } $linesHtml[] = '<div> </div>'; $contactHtml = implode('', $linesHtml); // Destinatario $clienteNombre = $empr['razon_social'] ?? ($empr['nombre_comercial'] ?? ($p['user_name'] ?? 'Cliente')); $clienteCiudad = $empr['localidad'] ?? ''; $clienteDepto = $empr['pais'] ?? ''; $clienteDoc = ''; if (is_array($empr) && !empty($empr['documento'])) { $clienteDoc = (string)$empr['documento']; } else { try { $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 */ } 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 */ } } } // Filas 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> <?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]); } }
Coded With 💗 by
0x6ick