Tul xxx Tul
User / IP
:
216.73.216.217
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_html
/
cynea2
/
views
/
Viewing: projects.php
<?php require_once __DIR__ . '/../utils/auth.php'; ?> <?php include __DIR__ . '/../includes/header.php'; ?> <?php include __DIR__ . '/../includes/sidebar.php'; ?> <?php require_once __DIR__ . '/../utils/helpers.php'; // Función PHP para obtener la clase CSS del badge de estado function getStatusBadgeClass($status) { switch ($status) { case 'Negociacion': return 'badge-negociacion'; case 'Planificacion': return 'badge-planificacion'; case 'Creacion': return 'badge-creacion'; case 'Revision': return 'badge-revision'; case 'Publicacion': return 'badge-publicacion'; default: return 'badge-secondary'; } } ?> <script src="https://cdn.jsdelivr.net/npm/tinymce@6.8.3/tinymce.min.js" referrerpolicy="origin"></script> <div class="main-content"> <div class="users-header"> <h1>Proyectos</h1> <button type="button" class="btn btn-primary" onclick="openProjectModal()">+ Nuevo Proyecto</button> </div> <form method="get" action="router.php" class="user-search-form"> <input type="hidden" name="action" value="projects"> <div class="search-group"> <div class="search-input"> <i class="fa fa-search"></i> <input type="text" name="nombre" placeholder="Buscar por nombre de proyecto..." value="<?= htmlspecialchars($_GET['nombre'] ?? '') ?>"> </div> <select name="cliente_id"> <option value="">Todos los clientes</option> <?php foreach ($clientes as $c): ?> <option value="<?= $c['id'] ?>" <?= (isset($_GET['cliente_id']) && $_GET['cliente_id'] == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['nombre'] ?: $c['razon_social']) ?></option> <?php endforeach; ?> </select> <select name="project_manager"> <option value="">Todos los managers</option> <?php foreach ($users as $u): ?> <option value="<?= $u['id'] ?>" <?= (isset($_GET['project_manager']) && $_GET['project_manager'] == $u['id']) ? 'selected' : '' ?>><?= htmlspecialchars($u['nombre']) ?></option> <?php endforeach; ?> </select> <select name="status"> <option value="">Todos los estados</option> <option value="Negociacion" <?= (isset($_GET['status']) && $_GET['status'] == 'Negociacion') ? 'selected' : '' ?>>Negociación</option> <option value="Planificacion" <?= (isset($_GET['status']) && $_GET['status'] == 'Planificacion') ? 'selected' : '' ?>>Planificación</option> <option value="Escritura" <?= (isset($_GET['status']) && $_GET['status'] == 'Escritura') ? 'selected' : '' ?>>Escritura</option> <option value="Creacion" <?= (isset($_GET['status']) && $_GET['status'] == 'Creacion') ? 'selected' : '' ?>>Creación</option> <option value="Revision" <?= (isset($_GET['status']) && $_GET['status'] == 'Revision') ? 'selected' : '' ?>>Revisión</option> <option value="Correcciones" <?= (isset($_GET['status']) && $_GET['status'] == 'Correcciones') ? 'selected' : '' ?>>Correcciones</option> <option value="Publicacion" <?= (isset($_GET['status']) && $_GET['status'] == 'Publicacion') ? 'selected' : '' ?>>Publicación</option> </select> <button type="submit" class="btn btn-search">Buscar</button> </div> </form> <div class="user-deck"> <?php foreach ($proyectos as $proyecto): if ( (empty($_GET['nombre']) || stripos($proyecto['proyecto'], $_GET['nombre']) !== false) && (empty($_GET['cliente_id']) || $proyecto['cliente_id'] == $_GET['cliente_id']) && (empty($_GET['project_manager']) || $proyecto['project_manager'] == $_GET['project_manager']) && (empty($_GET['status']) || $proyecto['status'] == $_GET['status']) ): ?> <div class="user-card project-card-full" onclick='openProjectSidebar(<?= json_encode($proyecto) ?>)' data-project-id="<?= $proyecto['id'] ?>"> <img src="<?= isset($proyecto['cliente_imagen']) && $proyecto['cliente_imagen'] ? 'uploads/clients/' . htmlspecialchars($proyecto['cliente_imagen']) : 'assets/img/user-default.png' ?>" class="user-avatar" onerror="this.src='assets/img/user-default.png'"> <div class="user-info"> <strong><?= htmlspecialchars($proyecto['proyecto']) ?></strong> <span class="badge <?= getStatusBadgeClass($proyecto['status']) ?>"> <?= htmlspecialchars($proyecto['status']) ?> </span> <div>Cliente: <?= htmlspecialchars($proyecto['cliente_nombre'] ?: $proyecto['razon_social']) ?></div> <div>Project Manager: <?= htmlspecialchars($proyecto['manager_nombre']) ?></div> <div>Inicio: <?= formatDate($proyecto['inicio']) ?> | Fin: <?= formatDate($proyecto['fin']) ?></div> </div> </div> <?php endif; endforeach; ?> </div> <div id="projectSidebar" class="project-sidebar"> <div class="sidebar-content"> <span class="close-project" onclick="closeProjectSidebar()">×</span> <div id="sidebar-header"></div> <div class="sidebar-tabs"> <button class="tab-btn active" onclick="showSidebarTab('info')"><i class="fa fa-tasks"></i> Acciones</button> <button class="tab-btn" onclick="showSidebarTab('productos')"><i class="fa-solid fa-file-invoice-dollar"></i> Facturación</button> <button class="tab-btn" onclick="showSidebarTab('pagos')"><i class="fa fa-money-bill"></i> Pagos</button> <button class="tab-btn" onclick="showSidebarTab('obs')"><i class="fa fa-comment"></i> Contenidos</button> </div> <div id="sidebar-tab-info" class="sidebar-tab-content"></div> <div id="sidebar-tab-productos" class="sidebar-tab-content" style="display:none;"></div> <div id="sidebar-tab-pagos" class="sidebar-tab-content" style="display:none;"></div> <div id="sidebar-tab-obs" class="sidebar-tab-content" style="display:none;"> <div id="contenidos-lista"></div> <form id="contenidoForm" enctype="multipart/form-data" style="margin-top:20px;"> <div class="row g-3 mb-3"> <div class="col-md-4"> <input type="hidden" name="proyecto_id" value="" id="contenido-proyecto-id"> <input type="text" name="contenido" class="form-control" placeholder="Descripción del contenido" required> </div> <div class="col-md-4"> <input type="file" name="archivo" class="form-control" accept="image/*,video/*,audio/*,application/pdf"> </div> <div class="col-md-4"> <input type="url" name="url" class="form-control" placeholder="URL (opcional)"> </div> </div> <button type="submit" class="btn btn-primary w-100">Agregar Contenido</button> </form> </div> </div> </div> <!-- Modal Proyecto --> <div id="projectModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 800px; min-width: 600px; overflow-y:auto; max-height:90vh;"> <span class="close-project" onclick="closeProjectModal()">×</span> <form id="projectForm" action="router.php?action=createProject" method="post" class="user-form-modal" onsubmit="return validateProjectForm()"> <input type="hidden" name="id" id="project-id"> <div class="row g-3 mb-3"> <div class="col-md-6"> <label class="form-label">Cliente:</label> <select name="cliente_id" id="project-cliente" class="form-select"> <?php foreach ($clientes as $c): ?> <option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['nombre'] ?: $c['razon_social']) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-6"> <label class="form-label">Project Manager:</label> <select name="project_manager" id="project-manager" class="form-select"> <?php foreach ($users as $u): ?> <option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['nombre']) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-6"> <label class="form-label">Proyecto:</label> <input type="text" name="proyecto" id="project-nombre" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Estado:</label> <select name="status" id="project-status" class="form-select"> <option value="Negociacion">Negociación</option> <option value="Planificacion">Planificación</option> <option value="Creacion">Creación</option> <option value="Correcciones">Correcciones</option> <option value="Publicacion">Publicación</option> </select> </div> <div class="col-md-6"> <label class="form-label">Inicio:</label> <input type="date" name="inicio" id="project-inicio" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Fin:</label> <input type="date" name="fin" id="project-fin" class="form-control"> </div> <div class="col-12"> <label class="form-label">Condiciones del Contrato:</label> <textarea name="condiciones_contrato" id="project-condiciones" class="form-control" rows="6"></textarea> <small class="text-muted">Se usará la plantilla predeterminada si se deja vacío.</small> </div> </div> <button type="submit" class="btn btn-primary w-100" id="projectFormBtn">Guardar Proyecto</button> </form> </div> </div> <!-- Modal Editar Proyecto --> <div id="editarProyectoModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 700px; min-width: 400px;"> <span class="close-project" onclick="closeEditarProyectoModal()">×</span> <form id="editarProyectoForm" action="router.php?action=updateProject" method="post" class="user-form-modal" onsubmit="return validateProjectFormEdit()"> <input type="hidden" name="id" id="editar-project-id"> <div class="row g-3 mb-3"> <div class="col-md-6"> <label class="form-label">Cliente:</label> <select name="cliente_id" id="editar-project-cliente" class="form-select"> <?php foreach ($clientes as $c): ?> <option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['nombre'] ?: $c['razon_social']) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-6"> <label class="form-label">Project Manager:</label> <select name="project_manager" id="editar-project-manager" class="form-select"> <?php foreach ($users as $u): ?> <option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['nombre']) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-6"> <label class="form-label">Proyecto:</label> <input type="text" name="proyecto" id="editar-project-nombre" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Estado:</label> <select name="status" id="editar-project-status" class="form-select"> <option value="Negociacion">Negociación</option> <option value="Planificacion">Planificación</option> <option value="Creacion">Creación</option> <option value="Correcciones">Correcciones</option> <option value="Publicacion">Publicación</option> </select> </div> <div class="col-md-6"> <label class="form-label">Inicio:</label> <input type="date" name="inicio" id="editar-project-inicio" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Fin:</label> <input type="date" name="fin" id="editar-project-fin" class="form-control"> </div> <div class="col-12"> <label class="form-label">Condiciones del Contrato:</label> <textarea name="condiciones_contrato" id="editar-project-condiciones" class="form-control" rows="6"></textarea> <small class="text-muted">Personaliza las condiciones para este proyecto.</small> </div> </div> <button type="submit" class="btn btn-primary w-100">Actualizar Proyecto</button> </form> </div> </div> <!-- Modal Productos --> <div id="productosModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 700px; min-width: 400px;"> <span class="close-project" onclick="closeProductosModal()">×</span> <div id="productos-modal-content"></div> </div> </div> <!-- Modal Acciones --> <div id="accionModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 700px; min-width: 400px;"> <span class="close-project" onclick="closeAccionModal()">×</span> <form id="accionForm" action="router.php?action=saveAccion" method="post" class="user-form-modal"> <input type="hidden" name="id" id="accion-id"> <input type="hidden" name="proyecto_id" id="accion-proyecto-id"> <div class="row g-3 mb-3"> <div class="col-md-9"> <label class="form-label">Producto del Proyecto (opcional):</label> <select id="accion-producto-select" name="producto_id" class="form-select"> <option value="">-- Selecciona un producto para autocompletar --</option> </select> </div> <div class="col-md-3" id="accion-producto-cantidad-group" style="display:none;"> <label class="form-label">Cantidad:</label> <input type="number" id="accion-producto-cantidad" name="cantidad" class="form-control" min="1" value="1"> </div> <div class="col-md-12"> <label class="form-label">Acción:</label> <input type="text" name="accion" id="accion-nombre" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Inicio:</label> <input type="datetime-local" name="inicio" id="accion-inicio" class="form-control" required> </div> <div class="col-md-6"> <label class="form-label">Fin:</label> <input type="datetime-local" name="fin" id="accion-fin" class="form-control"> </div> <div class="col-md-6"> <label class="form-label">Estado:</label> <select name="status" id="accion-status" class="form-select"> <option value="Pendiente">Pendiente</option> <option value="En Progreso">En Progreso</option> <option value="Lograda">Lograda</option> </select> </div> <div class="col-md-6"> <label class="form-label">Responsable:</label> <select name="responsable_id" id="accion-responsable" class="form-select" required> <option value="">Seleccionar responsable...</option> <optgroup label="Usuarios"> <?php foreach ($users as $u): ?> <option value="user_<?= $u['id'] ?>"><?= htmlspecialchars($u['nombre']) ?></option> <?php endforeach; ?> </optgroup> <optgroup label="Clientes"> <?php foreach ($clientes as $c): ?> <option value="client_<?= $c['id'] ?>"><?= htmlspecialchars($c['nombre'] ?: $c['razon_social']) ?></option> <?php endforeach; ?> </optgroup> </select> </div> <div class="col-md-12"> <label class="form-label">Participantes:</label> <div id="accion-participantes-checkboxes"> <div class="form-label">Usuarios:</div> <?php foreach ($users as $u): ?> <div class="form-check form-check-inline"> <input class="form-check-input" type="checkbox" name="participantes[]" id="participante-user-<?= $u['id'] ?>" value="user_<?= $u['id'] ?>"> <label class="form-check-label" for="participante-user-<?= $u['id'] ?>"><?= htmlspecialchars($u['nombre']) ?></label> </div> <?php endforeach; ?> <div class="form-label mt-2">Clientes:</div> <?php foreach ($clientes as $c): ?> <div class="form-check form-check-inline"> <input class="form-check-input" type="checkbox" name="participantes[]" id="participante-client-<?= $c['id'] ?>" value="client_<?= $c['id'] ?>"> <label class="form-check-label" for="participante-client-<?= $c['id'] ?>"><?= htmlspecialchars($c['nombre'] ?: $c['razon_social']) ?></label> </div> <?php endforeach; ?> </div> </div> </div> <button type="submit" class="btn btn-primary w-100">Guardar Acción</button> </form> </div> </div> <!-- Modal para previsualización de archivos multimedia de contenidos --> <div id="contenidoPreviewModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 600px; min-width: 320px;"> <span class="close-project" onclick="closeContenidoPreview()">×</span> <div id="contenido-preview-body" style="text-align:center;"></div> </div> </div> <!-- Modal Calendario Proyecto --> <div id="projectCalendarModal" class="modal-project" style="display:none;"> <div class="modal-content-user" style="max-width: 900px; min-width: 400px;"> <span class="close-project" onclick="closeProjectCalendarModal()">×</span> <h2 style="color:#00bfff;text-align:center;margin-bottom:18px;">Calendario de Acciones</h2> <div id="calendar-controls" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> <button onclick="changeCalendarMonth(-1)" class="btn contenido-arrow-btn"><</button> <span id="calendar-month-label" style="font-size:1.2em;font-weight:600;color:#222;"></span> <button onclick="changeCalendarMonth(1)" class="btn contenido-arrow-btn">></button> </div> <div id="calendar-grid"></div> </div> </div> </div> <?php include __DIR__ . '/../includes/footer.php'; ?> <script> // --- Lógica para productos y totales --- let detallesTemp = []; const defaultContractHtml = `<ol> <li><b>Objeto:</b> CyNe se compromete a crear un paquete de contenidos según lo incluido en esta factura.</li> <li><b>Plazo de Entrega:</b> El tiempo de entrega será de dos días a partir de la firma, con entregas parciales y revisiones según acuerdo.</li> <li><b>Condiciones de Pago:</b> El cliente se compromete a pagar el 50% al inicio y el 50% al finalizar, salvo acuerdo diferente.</li> <li><b>Revisión de Contenidos:</b> El cliente tiene derecho a una revisión de guiones y una corrección adicional tras la entrega.</li> <li><b>Responsabilidades del Cliente:</b> Brindar toda la información necesaria para la realización de los contenidos.</li> <li><b>Análisis de Mercado:</b> CyNe realizará un análisis de mercado con herramientas propias.</li> <li><b>Derechos de Autor:</b> Se ceden derechos de explotación al cliente, CyNe se reserva derechos de creación.</li> <li><b>Confidencialidad:</b> CyNe mantendrá la confidencialidad de los datos del cliente.</li> <li><b>Modificaciones:</b> Cualquier modificación debe ser consentida por ambas partes y documentada.</li> <li><b>Firma Electrónica:</b> Las firmas electrónicas tienen la misma validez legal que las manuscritas.</li> <li><b>Fuerza Mayor:</b> Ninguna parte será responsable por incumplimiento debido a fuerza mayor.</li> </ol>`; function contractHtmlToPlainText(html) { if (!html) return ''; const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const items = tempDiv.querySelectorAll('li'); if (items.length) { return Array.from(items).map(li => li.textContent.trim()).filter(Boolean).join('\n'); } return tempDiv.textContent.trim(); } function escapeHtml(str) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return str.replace(/[&<>"']/g, ch => map[ch]); } function plainTextToContractHtml(text) { if (!text) return ''; if (/<(li|ol|ul|p|br|div)\b/i.test(text)) { return text; } const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean); if (!lines.length) return ''; const items = lines.map(line => `<li>${escapeHtml(line)}</li>`); return `<ol>${items.join('')}</ol>`; } // Se llama al cambiar el producto seleccionado function actualizarPrecioProducto() { const select = document.getElementById('detalle-producto'); const precioUnitarioInput = document.getElementById('detalle-precio-unitario'); const selectedOption = select.options[select.selectedIndex]; const precio = selectedOption.getAttribute('data-precio') || '0'; precioUnitarioInput.value = parseFloat(precio).toFixed(2); calcularSubtotal(); calcularTotal(); } // Se llama al cambiar la cantidad o el precio unitario function calcularSubtotal() { const cantidad = parseFloat(document.getElementById('detalle-cantidad').value) || 0; const precioUnitario = parseFloat(document.getElementById('detalle-precio-unitario').value) || 0; const subtotalInput = document.getElementById('detalle-subtotal'); subtotalInput.value = (precioUnitario * cantidad).toFixed(2); } function agregarDetalleTemp() { const select = document.getElementById('detalle-producto'); const producto_id = select.value; if (!producto_id) { Swal.fire({ icon: 'warning', title: 'Por favor, selecciona un producto.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', zIndex: 9999, showConfirmButton: false, timer: 3000 }); return; } const producto_nombre = select.options[select.selectedIndex].text; const cantidad = parseFloat(document.getElementById('detalle-cantidad').value) || 1; const precio_unitario = parseFloat(document.getElementById('detalle-precio-unitario').value) || 0; const existingDetalleIndex = detallesTemp.findIndex(d => d.producto_id === producto_id); if (existingDetalleIndex > -1) { detallesTemp[existingDetalleIndex].cantidad += cantidad; detallesTemp[existingDetalleIndex].subtotal = detallesTemp[existingDetalleIndex].cantidad * detallesTemp[existingDetalleIndex].precio_unitario; } else { const subtotal = cantidad * precio_unitario; detallesTemp.push({ producto_id, producto_nombre, cantidad, precio_unitario, subtotal }); } renderDetalles(); calcularTotal(); document.getElementById('detalle-producto').value = ""; document.getElementById('detalle-cantidad').value = "1"; document.getElementById('detalle-precio-unitario').value = "0.00"; document.getElementById('detalle-subtotal').value = "0.00"; } function renderDetalles() { let html = '<table class="table table-striped"><thead><tr><th>Producto</th><th>Cantidad</th><th>Precio Unitario</th><th>Subtotal</th><th>Acciones</th></tr></thead><tbody>'; detallesTemp.forEach((d, index) => { html += `<tr> <td>${d.producto_nombre}</td> <td>${d.cantidad}</td> <td>$${d.precio_unitario.toFixed(2)}</td> <td>$${d.subtotal.toFixed(2)}</td> <td> <button type="button" class="btn btn-danger btn-sm" onclick="eliminarDetalleTemp(${index})">Eliminar</button> </td> </tr>`; }); html += '</tbody></table>'; document.getElementById('detalles-lista').innerHTML = html; } function eliminarDetalleTemp(index) { detallesTemp.splice(index, 1); renderDetalles(); calcularTotal(); } function calcularTotal() { let subtotalGeneral = detallesTemp.reduce((acc, d) => acc + d.subtotal, 0); document.getElementById('subtotal-original-proyecto').textContent = '$ ' + subtotalGeneral.toFixed(2); let descuentoPercent = parseFloat(document.getElementById('descuento').value) || 0; let descuentoMonto = subtotalGeneral * (descuentoPercent / 100); let totalFinal = subtotalGeneral - descuentoMonto; document.getElementById('total-proyecto').textContent = '$' + totalFinal.toFixed(2); calcularPendiente(); } function calcularPendiente() { let total = parseFloat(document.getElementById('total-proyecto').textContent.replace('$', '')) || 0; let abonado = 0; const pagos = document.querySelectorAll('#pagos-lista .pago-monto'); pagos.forEach(p => abonado += parseFloat(p.textContent.replace('$', '')) || 0); var elAbonado = document.getElementById('total-abonado'); var elPendiente = document.getElementById('total-pendiente'); if (elAbonado) elAbonado.textContent = '$' + abonado.toFixed(2); if (elPendiente) elPendiente.textContent = '$' + (total - abonado).toFixed(2); } // Event listeners para actualizar cálculos en tiempo real en el modal de Gestionar Productos function setupProductFormListeners() { const cantidadInput = document.getElementById('detalle-cantidad'); const productoSelect = document.getElementById('detalle-producto'); const descuentoInput = document.getElementById('descuento'); const precioUnitarioInput = document.getElementById('detalle-precio-unitario'); const subtotalInput = document.getElementById('detalle-subtotal'); if (!cantidadInput || !productoSelect || !descuentoInput || !precioUnitarioInput || !subtotalInput) { console.error("No se encontraron todos los elementos del formulario de producto para adjuntar listeners en el modal de productos."); return; } cantidadInput.removeEventListener('input', handleCantidadInput); productoSelect.removeEventListener('change', handleProductoChange); descuentoInput.removeEventListener('input', handleDescuentoInput); cantidadInput.addEventListener('input', handleCantidadInput); productoSelect.addEventListener('change', handleProductoChange); descuentoInput.addEventListener('input', handleDescuentoInput); actualizarPrecioProducto(); console.log("Event listeners para formulario de producto configurados en el modal de productos."); } function handleCantidadInput() { console.log("Cantidad cambiada en modal productos, recalculando..."); calcularSubtotal(); calcularTotal(); } function handleProductoChange() { console.log("Producto cambiado en modal productos, actualizando precio y recalculando..."); actualizarPrecioProducto(); } function handleDescuentoInput() { console.log("Descuento cambiado en modal productos, recalculando total..."); calcularTotal(); } // Asegurar que openProjectModal NO configure listeners de producto function openProjectModal() { document.getElementById('projectForm').reset(); document.getElementById('project-id').value = ''; document.getElementById('projectFormBtn').textContent = 'Guardar Proyecto'; document.getElementById('projectForm').action = 'router.php?action=createProject'; const contractTextarea = document.getElementById('project-condiciones'); if (contractTextarea) { contractTextarea.value = defaultContractHtml; } const editor = window.contractEditors && window.contractEditors.create; if (editor) { editor.setContent(defaultContractHtml); editor.undoManager.clear(); } document.getElementById('projectModal').style.display = 'flex'; } // Asegurar que openEditProjectModal NO configure listeners de producto function openEditProjectModal(proyecto) { document.getElementById('projectModal').style.display = 'flex'; } function closeProjectModal() { document.getElementById('projectModal').style.display = 'none'; } function validateProjectForm() { if (!document.getElementById('project-nombre').value.trim()) { alert('El nombre del proyecto es obligatorio'); return false; } return true; } document.getElementById('projectForm').onsubmit = function(e) { if (!validateProjectForm()) { return false; } if (window.contractEditors && window.contractEditors.create) { e.target.elements['condiciones_contrato'].value = window.contractEditors.create.getContent(); } return true; } window.onclick = function(event) { let modal = document.getElementById('projectModal'); if (event.target == modal) { closeProjectModal(); } } function formatDateTime(datetimeString) { if (!datetimeString) return '-'; const date = new Date(datetimeString); if (isNaN(date.getTime())) return '-'; const days = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado']; const dayOfWeek = days[date.getDay()]; const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear(); let hours = date.getHours(); const minutes = date.getMinutes().toString().padStart(2, '0'); const ampm = hours >= 12 ? 'pm' : 'am'; hours = hours % 12; hours = hours ? hours : 12; return `${dayOfWeek} ${day}-${month}-${year} ${hours}:${minutes} ${ampm}`; } function openProjectSidebar(proyecto) { window.currentProject = proyecto; const sidebar = document.getElementById('projectSidebar'); let statusClass = getStatusBadgeClass(proyecto.status); let header = `<div class='sidebar-header'>`; header += `<div style='display:flex;flex-direction:column;align-items:center;'>`; header += `<img src='${proyecto.cliente_imagen ? 'uploads/clients/' + proyecto.cliente_imagen : 'assets/img/user-default.png'}' class='user-avatar' onerror="this.src='assets/img/user-default.png'">`; header += `<div style='width:100%;display:flex;justify-content:center;'> <span class='badge sidebar-badge ${statusClass}' style='margin-top:10px;cursor:pointer;' onclick='showProjectStatusDropdown()' id='sidebar-project-status-badge'>${proyecto.status}</span> </div>`; header += `</div>`; header += `<div class='header-info'>`; header += `<div class='header-title'>${proyecto.proyecto}</div>`; header += `<div class='header-client'><i class='fa fa-user'></i> ${proyecto.cliente_nombre || proyecto.razon_social}</div>`; header += `<div class='header-manager'><i class='fa fa-user-tie'></i> ${proyecto.manager_nombre}</div>`; header += `<div class='header-actions'>`; header += `<button type='button' class='btn-icon-round' id='sidebar-edit-btn' title='Editar Proyecto' onclick='openEditProyectoSoloDatos(window.currentProject)'><i class='fa fa-pen'></i></button>`; header += `<button type='button' class='btn-icon-round actions' onclick='openAccionModal(window.currentProject.id)' title='Nueva Acción'><i class="fa fa-tasks"></i></button>`; header += `<button type='button' class='btn-icon-round calendar' onclick='openProjectCalendar(window.currentProject.id)' title='Calendario'><i class="fa fa-calendar-days"></i></button>`; header += `<button type='button' class='btn-icon-round products' id='sidebar-productos-btn' title='Gestionar Productos' onclick='openProductosModal(window.currentProject)'><i class="fa-solid fa-file-invoice-dollar"></i></button>`; header += `<button type='button' class='btn-icon-round delete' id='sidebar-delete-btn' title='Eliminar Proyecto' onclick='deleteProjectById(window.currentProject.id)'><i class='fa fa-trash'></i></button>`; header += `</div>`; header += `</div></div>`; document.getElementById('sidebar-header').innerHTML = header; document.getElementById('sidebar-tab-info').innerHTML = ` <div style="text-align:center; margin-bottom: 10px;"> <b>Inicio:</b> ${formatDateOnly(proyecto.inicio)} <b>| Fin:</b> ${formatDateOnly(proyecto.fin)} </div> <div class="acciones-container"> <div id="acciones-lista-${proyecto.id}" class="acciones-lista"></div> </div> `; document.getElementById('sidebar-tab-productos').innerHTML = ` <div id="facturacion-details-${proyecto.id}">Cargando detalles de factura...</div> `; document.getElementById('sidebar-tab-pagos').innerHTML = `<div style='color:#888;'>Aquí se mostrarán los pagos del proyecto.</div>`; showSidebarTab('info'); sidebar.classList.add('open'); document.body.classList.add('sidebar-open'); loadAcciones(proyecto.id); document.getElementById('sidebar-edit-btn').onclick = function() { openEditProyectoSoloDatos(window.currentProject); }; document.getElementById('sidebar-productos-btn').onclick = function() { openProductosModal(window.currentProject); }; document.getElementById('sidebar-delete-btn').onclick = function() { deleteProjectById(window.currentProject.id); }; // Asegurar que el campo hidden del formulario de contenidos tenga el ID del proyecto const contenidoProyectoId = document.getElementById('contenido-proyecto-id'); if (contenidoProyectoId) { contenidoProyectoId.value = proyecto.id; } } function openEditProyectoSoloDatos(proyecto) { console.log("Abriendo modal de edición de proyecto para ID:", proyecto.id); // Debugging const form = document.getElementById('editarProyectoForm'); if (!form) { console.error("Formulario de edición (editarProyectoForm) no encontrado."); Swal.fire({ icon: 'error', title: 'Error', text: 'No se pudo encontrar el formulario de edición.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); return; } form.reset(); const projectIdInput = document.getElementById('editar-project-id'); if (projectIdInput) { projectIdInput.value = proyecto.id; } else { console.error("Elemento editar-project-id no encontrado."); } const clienteSelect = document.getElementById('editar-project-cliente'); if (clienteSelect) { clienteSelect.value = proyecto.cliente_id; } else { console.error("Elemento editar-project-cliente no encontrado."); } const managerSelect = document.getElementById('editar-project-manager'); if (managerSelect) { managerSelect.value = proyecto.project_manager; } else { console.error("Elemento editar-project-manager no encontrado."); } const nombreInput = document.getElementById('editar-project-nombre'); if (nombreInput) { nombreInput.value = proyecto.proyecto; } else { console.error("Elemento editar-project-nombre no encontrado."); } const statusSelect = document.getElementById('editar-project-status'); if (statusSelect) { statusSelect.value = proyecto.status; } else { console.error("Elemento editar-project-status no encontrado."); } const inicioInput = document.getElementById('editar-project-inicio'); if (inicioInput) { inicioInput.value = proyecto.inicio; } else { console.error("Elemento editar-project-inicio no encontrado."); } const finInput = document.getElementById('editar-project-fin'); if (finInput) { finInput.value = proyecto.fin; } else { console.error("Elemento editar-project-fin no encontrado."); } const condicionesTextarea = document.getElementById('editar-project-condiciones'); if (condicionesTextarea) { const htmlFromProject = proyecto.condiciones_contrato || plainTextToContractHtml(proyecto.condiciones_contrato_textarea || proyecto.condiciones_contrato_plain || ''); const finalHtml = (htmlFromProject && contractHtmlToPlainText(htmlFromProject)) ? htmlFromProject : defaultContractHtml; condicionesTextarea.value = finalHtml; const editor = window.contractEditors && window.contractEditors.edit; if (editor) { editor.setContent(finalHtml); editor.undoManager.clear(); } } else { console.error("Elemento editar-project-condiciones no encontrado."); } const sidebar = document.getElementById('projectSidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.dataset.locked = '1'; } document.getElementById('editarProyectoModal').style.display = 'flex'; console.log("Modal de edición mostrado."); // Debugging } function closeEditarProyectoModal() { document.getElementById('editarProyectoModal').style.display = 'none'; const sidebar = document.getElementById('projectSidebar'); if (sidebar) { delete sidebar.dataset.locked; } } function openProductosModal(proyecto) { detallesTemp = []; document.getElementById('productos-modal-content').innerHTML = ` <h3>Productos del Proyecto</h3> <form id='productosForm' onsubmit="event.preventDefault(); agregarDetalleTemp();"> <div class='row g-3 mb-3'> <div class='col-md-6'> <label class='form-label'>Producto:</label> <select name='producto_id' id='detalle-producto' class='form-select'> <option value=''>Seleccionar producto...</option> <?php foreach ($productos as $p): ?> <option value='<?= $p['id'] ?>' data-precio='<?= $p['precio'] ?>'><?= htmlspecialchars($p['producto']) ?></option> <?php endforeach; ?> </select> </div> <div class='col-md-3'> <label class='form-label'>Cantidad:</label> <input type='number' name='cantidad' id='detalle-cantidad' class='form-control' min='1' value='1'> </div> <div class='col-md-3'> <label class='form-label'>Precio Unitario:</label> <input type='number' name='precio_unitario' id='detalle-precio-unitario' class='form-control' min='0' step='0.01' readonly> </div> </div> <div class="row g-3 mb-3"> <div class="col-md-6"> <label class="form-label">Subtotal Producto:</label> <input type="number" name="subtotal" id="detalle-subtotal" class="form-control" min="0" step="0.01" readonly> </div> </div> <button type='button' class='btn btn-primary w-100' onclick='agregarDetalleTemp()' >Agregar Producto</button> <div style='margin-top:10px;'> <div><b>Subtotal:</b> <span id='subtotal-original-proyecto' style='font-weight:bold; color:#0099e5;'>$0</span></div> <label>Descuento (%):</label> <input type='number' id='descuento' class='form-control' style='width:120px;display:inline-block;' value='0' min='0'> <label style='margin-left:10px;'>Total:</label> <span id='total-proyecto' style='font-weight:bold; color:#0099e5;'>$0</span> </div> </form> <div id='detalles-lista'></div> <button type="button" class="btn btn-success w-100 mt-3" onclick="saveProjectDetalles(window.currentProject.id)">Guardar Productos</button> `; const sidebar = document.getElementById('projectSidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.dataset.locked = '1'; } showSidebarTab('productos'); document.getElementById('productosModal').style.display = 'flex'; setupProductFormListeners(); loadProjectDetalles(proyecto.id); } function closeProductosModal() { document.getElementById('productosModal').style.display = 'none'; const sidebar = document.getElementById('projectSidebar'); if (sidebar) { setTimeout(() => { if (document.getElementById('productosModal').style.display === 'none') { delete sidebar.dataset.locked; } }, 200); } } function closeProjectSidebar() { const sidebar = document.getElementById('projectSidebar'); if (!sidebar || sidebar.dataset.locked === '1') { return; } sidebar.classList.remove('open'); document.body.classList.remove('sidebar-open'); } function validateProjectFormEdit() { if (!document.getElementById('editar-project-nombre').value.trim()) { Swal.fire({ icon: 'warning', title: 'El nombre del proyecto es obligatorio', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', zIndex: 9999 }); return false; } return true; } document.getElementById('editarProyectoForm').onsubmit = function(e) { e.preventDefault(); // Prevenir el envío del formulario tradicional const form = e.target; if (window.contractEditors && window.contractEditors.edit) { form.elements['condiciones_contrato'].value = window.contractEditors.edit.getContent(); } const formData = new FormData(form); const proyectoId = document.getElementById('editar-project-id').value; // Validar formulario si es necesario if (!validateProjectFormEdit()) { return; } fetch(form.action, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.success) { Swal.fire({ icon: 'success', title: '¡Proyecto actualizado!', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', showConfirmButton: false, timer: 3000 // Se cierra automáticamente }); // Actualizar los datos del proyecto en la variable global window.currentProject // Y luego actualizar visualmente la sidebar window.currentProject.cliente_id = formData.get('cliente_id'); window.currentProject.project_manager = formData.get('project_manager'); window.currentProject.proyecto = formData.get('proyecto'); window.currentProject.status = formData.get('status'); window.currentProject.inicio = formData.get('inicio'); window.currentProject.fin = formData.get('fin'); // Obtener los nombres actualizados del cliente y manager desde los selects para mostrarlos en la sidebar const clienteSelect = document.getElementById('editar-project-cliente'); window.currentProject.cliente_nombre = clienteSelect.options[clienteSelect.selectedIndex].text; const managerSelect = document.getElementById('editar-project-manager'); window.currentProject.manager_nombre = managerSelect.options[managerSelect.selectedIndex].text; // Re-renderizar el encabezado de la sidebar con los datos actualizados updateSidebarHeader(window.currentProject); // Actualizar la tarjeta del proyecto en el listado principal const projectCard = document.querySelector(`.project-card-full[data-project-id="${proyectoId}"]`); if (projectCard) { const userInfo = projectCard.querySelector('.user-info'); if (userInfo) { // Actualizar Nombre del proyecto const projectNameElement = userInfo.querySelector('strong'); if (projectNameElement) { projectNameElement.textContent = window.currentProject.proyecto; } // Actualizar Badge de Estado const statusBadgeElement = userInfo.querySelector('.badge'); if (statusBadgeElement) { statusBadgeElement.textContent = ' ' + window.currentProject.status + ' '; // Mantener espacios si existían // Eliminar clases de estado antiguas y añadir la nueva statusBadgeElement.classList.remove('badge-negociacion', 'badge-planificacion', 'badge-escritura', 'badge-creacion', 'badge-revision', 'badge-secondary'); statusBadgeElement.classList.add(getStatusBadgeClass(window.currentProject.status)); // Asegurar que el estilo margin-left se mantenga statusBadgeElement.style.marginLeft = '8px'; } // Actualizar Cliente, Manager y Fechas buscando por contenido o estructura const infoDivs = userInfo.querySelectorAll('div'); infoDivs.forEach(div => { const text = div.textContent; if (text.startsWith('Cliente:')) { div.innerHTML = `Cliente: ${window.currentProject.cliente_nombre || window.currentProject.razon_social}`; } else if (text.startsWith('Project Manager:')) { div.innerHTML = `Project Manager: ${window.currentProject.manager_nombre}`; } else if (text.includes('Inicio:') && text.includes('| Fin:')) { // Reutilizar la función formatDateTime si aplica, o solo mostrar las fechas crudas div.innerHTML = `Inicio: ${window.currentProject.inicio || '-'} | Fin: ${window.currentProject.fin || '-'}`; } // Podríamos añadir más lógica aquí para otros campos si es necesario }); } } closeEditarProyectoModal(); // Cerrar el modal de edición } else { Swal.fire({ icon: 'error', title: 'Error al actualizar', text: result.message || 'No se pudieron actualizar los datos del proyecto.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); } }) .catch(error => { console.error('Error updating project:', error); Swal.fire({ icon: 'error', title: 'Error de conexión', text: 'Ocurrió un error al intentar actualizar el proyecto.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); }); }; // Nueva función para actualizar el encabezado de la sidebar con los datos del proyecto function updateSidebarHeader(proyecto) { let statusClass = getStatusBadgeClass(proyecto.status); let header = `<div class='sidebar-header'>`; header += `<div style='display:flex;flex-direction:column;align-items:center;'>`; header += `<img src='${proyecto.cliente_imagen ? 'uploads/clients/' + proyecto.cliente_imagen : 'assets/img/user-default.png'}' class='user-avatar' onerror="this.src='assets/img/user-default.png'">`; header += `<div style='width:100%;display:flex;justify-content:center;'> <span class='badge sidebar-badge ${statusClass}' style='margin-top:10px;cursor:pointer;' onclick='showProjectStatusDropdown()' id='sidebar-project-status-badge'>${proyecto.status}</span> </div>`; header += `</div>`; header += `<div class='header-info'>`; header += `<div class='header-title'>${proyecto.proyecto}</div>`; header += `<div class='header-client'><i class='fa fa-user'></i> ${proyecto.cliente_nombre || proyecto.razon_social}</div>`; header += `<div class='header-manager'><i class='fa fa-user-tie'></i> ${proyecto.manager_nombre}</div>`; header += `<div class='header-actions'>`; header += `<button type='button' class='btn-icon-round' id='sidebar-edit-btn' title='Editar Proyecto' onclick='openEditProyectoSoloDatos(window.currentProject)'><i class='fa fa-pen'></i></button>`; header += `<button type='button' class='btn-icon-round actions' onclick='openAccionModal(window.currentProject.id)' title='Nueva Acción'><i class="fa fa-tasks"></i></button>`; header += `<button type='button' class='btn-icon-round calendar' onclick='openProjectCalendar(window.currentProject.id)' title='Calendario'><i class="fa fa-calendar-days"></i></button>`; header += `<button type='button' class='btn-icon-round products' id='sidebar-productos-btn' title='Gestionar Productos' onclick='openProductosModal(window.currentProject)'><i class='fa fa-box'></i></button>`; header += `<button type='button' class='btn-icon-round delete' id='sidebar-delete-btn' title='Eliminar Proyecto' onclick='deleteProjectById(window.currentProject.id)'><i class='fa fa-trash'></i></button>`; header += `</div>`; header += `</div></div>`; document.getElementById('sidebar-header').innerHTML = header; } // Asegúrate de que deleteProjectById exista si usas el onclick directo function deleteProjectById(proyectoId) { Swal.fire({ title: '¿Eliminar proyecto?', text: 'Esta acción no se puede deshacer', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#23252b', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', background: '#000', color: '#fff', }).then((result) => { if (result.isConfirmed) { const form = document.createElement('form'); form.method = 'post'; form.action = 'router.php?action=deleteProject'; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'id'; input.value = proyectoId; form.appendChild(input); document.body.appendChild(form); form.submit(); } }); } function showSidebarTab(tab) { document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.sidebar-tab-content').forEach(div => div.style.display = 'none'); document.querySelector('.tab-btn[onclick*="' + tab + '"]').classList.add('active'); document.getElementById('sidebar-tab-' + tab).style.display = 'block'; if (tab === 'productos' && window.currentProject) { loadFacturacion(window.currentProject.id); } else if (tab === 'pagos' && window.currentProject) { loadPagos(window.currentProject.id); } else if (tab === 'obs' && window.currentProject) { loadContenidos(window.currentProject.id); } } let productosProyecto = {}; function openAccionModal(proyectoId) { document.getElementById('accionForm').reset(); document.getElementById('accion-id').value = ''; const sidebar = document.getElementById('projectSidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.dataset.locked = '1'; } document.getElementById('accionModal').style.display = 'flex'; loadAcciones(proyectoId); // Llenar el select de productos del proyecto y guardar cantidades máximas Promise.all([ fetch(`router.php?action=getProjectDetails&proyecto_id=${proyectoId}`).then(r => r.json()), fetch(`router.php?action=getAcciones&proyecto_id=${proyectoId}`).then(r => r.json()) ]).then(([data, acciones]) => { const select = document.getElementById('accion-producto-select'); select.innerHTML = '<option value="">-- Selecciona un producto para autocompletar --</option>'; productosProyecto = {}; if (data && data.detalles && Array.isArray(data.detalles)) { data.detalles.forEach(det => { // Calcular horas ya usadas de este producto en acciones let usadas = 0; acciones.forEach(a => { if (a.producto_id && det.producto_id && String(a.producto_id) === String(det.producto_id)) { usadas += parseInt(a.cantidad) || 0; } }); const disponibles = det.cantidad - usadas; productosProyecto[det.producto_nombre] = { cantidad: disponibles, producto_id: det.producto_id }; if (disponibles > 0) { select.innerHTML += `<option value="${det.producto_id}">${det.producto_nombre} (disponibles: ${disponibles})</option>`; } }); } // Reset cantidad group document.getElementById('accion-producto-cantidad-group').style.display = 'none'; document.getElementById('accion-producto-cantidad').value = 1; }); } function closeAccionModal() { document.getElementById('accionModal').style.display = 'none'; const sidebar = document.getElementById('projectSidebar'); if (sidebar) { setTimeout(() => { if (document.getElementById('accionModal').style.display === 'none') { delete sidebar.dataset.locked; } }, 200); } } function loadAcciones(proyectoId) { fetch(`router.php?action=getAcciones&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(acciones => { // Ordenar por fecha de inicio ascendente acciones.sort((a, b) => new Date(a.inicio) - new Date(b.inicio)); const container = document.getElementById(`acciones-lista-${proyectoId}`); if (!container) { console.error('Contenedor de acciones no encontrado:', `acciones-lista-${proyectoId}`); return; } if (acciones.length === 0) { container.innerHTML = '<div class="no-acciones">No hay acciones registradas</div>'; return; } let html = '<div class="acciones-deck">'; const now = new Date(); acciones.forEach(accion => { let statusClass = getStatusClass(accion.status); let statusColor = ''; let statusIcon = ''; switch (statusClass) { case 'danger': statusColor = '#dc3545'; statusIcon = '<i class=\'fa fa-clock\'></i>'; break; case 'warning': statusColor = '#ffc107'; statusIcon = '<i class=\'fa-solid fa-gear\'></i>'; break; case 'success': statusColor = '#28a745'; statusIcon = '<i class=\'fa fa-check\'></i>'; break; default: statusColor = '#bdbdbd'; statusIcon = '<i class=\'fa fa-dot-circle\'></i>'; break; } // Colores pastel según reglas ajustadas let bg = '#f8fafc'; const inicio = new Date(accion.inicio); const hoy = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const inicioDia = new Date(inicio.getFullYear(), inicio.getMonth(), inicio.getDate()); const diffDias = Math.floor((inicioDia - hoy) / (1000*60*60*24)); if (accion.status === 'Lograda') { bg = '#e6fbe6'; // verde pastel } else if (diffDias === 0) { bg = '#e6f3fb'; // azul pastel (hoy) } else if (diffDias > 0 && diffDias <= 7) { bg = '#fffbe6'; // amarillo pastel (mañana a 7 días) } else if (diffDias < 0) { bg = '#fdeaea'; // rojo pastel (ayer o pasado) } const d1 = new Date(accion.inicio); const d2 = new Date(accion.fin); const sameDay = d1.toDateString() === d2.toDateString(); html += `<div class="accion-card" id="accion-card-${accion.id}" style="background:${bg};border-radius:16px;padding:18px 22px 16px 22px;box-shadow:0 2px 8px rgba(0,0,0,0.06);margin-bottom:18px;position:relative;"> <div style="display:flex;align-items:center;justify-content:space-between;"> <div style="display:flex;align-items:center;gap:10px;"> <span id="status-dropdown-trigger-${accion.id}" style="display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:${statusColor};color:#fff;font-size:1em;margin-right:10px;cursor:pointer;position:relative;" onclick="toggleStatusDropdown(event, ${accion.id}, '${accion.status}')">${statusIcon} <div id="status-dropdown-${accion.id}" class="status-dropdown" style="display:none;position:absolute;top:28px;left:0;z-index:9999;background:#23252b;border-radius:8px;box-shadow:0 2px 12px rgba(0,0,0,0.25);padding:6px 0;min-width:140px;"> <button onclick="changeAccionStatus(${accion.id}, 'Pendiente')" style="display:flex;align-items:center;gap:8px;background:none;border:none;color:#dc3545;width:100%;padding:8px 16px;font-size:1em;cursor:pointer;"><i class='fa fa-clock'></i> Pendiente</button> <button onclick="changeAccionStatus(${accion.id}, 'En Progreso')" style="display:flex;align-items:center;gap:8px;background:none;border:none;color:#ffc107;width:100%;padding:8px 16px;font-size:1em;cursor:pointer;"><i class=\"fa-solid fa-gear\"></i> En Progreso</button> <button onclick="changeAccionStatus(${accion.id}, 'Lograda')" style="display:flex;align-items:center;gap:8px;background:none;border:none;color:#28a745;width:100%;padding:8px 16px;font-size:1em;cursor:pointer;"><i class='fa fa-check'></i> Lograda</button> </div> </span> <span style="font-size:1.2em;font-weight:600;">${accion.accion}</span> </div> <div style="display:flex;align-items:center;gap:8px;"> <button type="button" class="btn-icon-round" onclick="editAccion(${accion.id})" title="Editar"><i class="fa fa-pen"></i></button> <button type="button" class="btn-icon-round delete" onclick="deleteAccion(${accion.id})" title="Eliminar"><i class="fa fa-trash"></i></button> </div> </div> <div style="margin:10px 0 4px 22px;color:#222;font-size:1em;"> ${sameDay ? `<span style='font-weight:700;'>${formatDateOnly(accion.inicio)}</span> <span style='margin:0 8px;color:#888;'>•</span> <span style='color:#555;'>${formatHourRange(accion.inicio, accion.fin)}</span>` : `<span style='color:#555;'>${formatHourRange(accion.inicio, accion.fin)}</span>`} </div> <div style="margin-left:22px;color:#888;font-size:0.98em;">Responsable: <b style='color:#222;'>${accion.responsable_nombre}</b></div> ${(Array.isArray(accion.participantes_nombres) && accion.participantes_nombres.length > 0) ? `<div style=\"margin-left:22px;color:#888;font-size:0.98em;\">Participantes: <b style='color:#222;'>${accion.participantes_nombres.join(', ')}</b></div>` : ''} </div>`; }); html += '</div>'; container.innerHTML = html; if (window.pendingAccionScroll) { scrollToAccion(window.pendingAccionScroll); delete window.pendingAccionScroll; } }) .catch(error => console.error('Error loading acciones:', error)); } function getStatusClass(status) { switch (status) { case 'Pendiente': return 'danger'; case 'En Progreso': return 'warning'; case 'Lograda': return 'success'; default: return 'secondary'; } } function editAccion(id) { fetch(`router.php?action=getAccion&id=${id}`) .then(response => response.json()) .then(accion => { document.getElementById('accion-id').value = accion.id; document.getElementById('accion-proyecto-id').value = accion.proyecto_id; document.getElementById('accion-nombre').value = accion.accion; document.getElementById('accion-inicio').value = accion.inicio ? accion.inicio.substring(0, 16) : ''; document.getElementById('accion-fin').value = accion.fin ? accion.fin.substring(0, 16) : ''; document.getElementById('accion-status').value = accion.status; document.getElementById('accion-responsable').value = accion.responsable_tipo + '_' + accion.responsable_id; const participantes = accion.participantes || []; document.querySelectorAll('#accion-participantes-checkboxes input[type="checkbox"]').forEach(checkbox => { checkbox.checked = participantes.includes(checkbox.value); }); const sidebar = document.getElementById('projectSidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.dataset.locked = '1'; } document.getElementById('accionModal').style.display = 'flex'; }); } function deleteAccion(id) { Swal.fire({ title: '¿Eliminar acción?', text: 'Esta acción no se puede deshacer', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#23252b', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', background: '#000', color: '#fff', }).then((result) => { if (result.isConfirmed) { fetch(`router.php?action=deleteAccion&id=${id}`, { method: 'POST' }) .then(response => response.json()) .then(result => { if (result.success) { loadAcciones(document.getElementById('accion-proyecto-id').value); } }); } }); } document.getElementById('accionForm').onsubmit = function(e) { e.preventDefault(); const inicio = document.getElementById('accion-inicio').value; const fin = document.getElementById('accion-fin').value; const proyectoInicio = window.currentProject ? window.currentProject.inicio : null; const proyectoFin = window.currentProject ? window.currentProject.fin : null; if (inicio && proyectoInicio && new Date(inicio) < new Date(proyectoInicio)) { Swal.fire({ icon: 'warning', title: 'Fecha inválida', text: 'La fecha de inicio de la acción no puede ser anterior al inicio del proyecto.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); return; } if (fin && proyectoFin && new Date(fin) > new Date(proyectoFin)) { Swal.fire({ icon: 'warning', title: 'Fecha inválida', text: 'La fecha de fin de la acción no puede ser posterior al fin del proyecto.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); return; } if (inicio && fin) { const d1 = new Date(inicio); const d2 = new Date(fin); if (d2 < d1) { Swal.fire({ icon: 'warning', title: 'Fechas inválidas', text: 'La fecha de fin no puede ser anterior a la de inicio.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); return; } } const formData = new FormData(this); fetch('router.php?action=saveAccion', { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.success) { closeAccionModal(); loadAcciones(formData.get('proyecto_id')); } }); }; function loadFacturacion(proyectoId) { fetch(`router.php?action=getProjectDetails&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(data => { const container = document.getElementById(`facturacion-details-${proyectoId}`); if (!container) { console.error('Contenedor de facturación no encontrado:', `facturacion-details-${proyectoId}`); return; } if (!data || data.detalles.length === 0) { container.innerHTML = '<div class="no-acciones">No hay productos registrados para este proyecto.</div>'; return; } let html = ` <table class="table table-striped"> <thead> <tr> <th>Producto</th> <th>Cantidad</th> <th>Precio Unitario</th> <th>Subtotal</th> </tr> </thead> <tbody> `; data.detalles.forEach(detalle => { html += ` <tr> <td>${detalle.producto_nombre}</td> <td>${detalle.cantidad}</td> <td>$${parseFloat(detalle.precio).toFixed(2)}</td> <td>$${(parseFloat(detalle.precio) * parseInt(detalle.cantidad)).toFixed(2)}</td> </tr> `; }); html += ` </tbody> </table> <div style="text-align:right; margin-top: 20px;"> <div><b>Subtotal:</b> $${parseFloat(data.subtotal_general || 0).toFixed(2)}</div> <div><b>Descuento (${parseFloat(data.descuento_porcentaje || 0).toFixed(2)}%):</b> $${parseFloat(data.descuento_monto || 0).toFixed(2)}</div> <div><b>Total Factura:</b> $${parseFloat(data.total_proyecto || 0).toFixed(2)}</div> </div> `; container.innerHTML = html; if (document.getElementById(`pagos-lista-${proyectoId}`)) { loadPagos(proyectoId); } }) .catch(error => console.error('Error loading facturacion:', error)); } function loadPagos(proyectoId) { const container = document.getElementById(`sidebar-tab-pagos`); if (!container) { console.error('Contenedor de pagos no encontrado:', `sidebar-tab-pagos`); return; } container.innerHTML = ` <h5>Historial de Pagos</h5> <div id='pagos-lista-${proyectoId}'>Cargando historial de pagos...</div> <div style='margin-top:15px; border-top: 1px solid #eee; padding-top: 10px;'> <div><b>Total Abonado:</b> <span id='total-abonado-${proyectoId}' style='font-weight:bold; color:#28a745;'>$0</span></div> <div><b>Saldo Pendiente:</b> <span id='total-pendiente-${proyectoId}' style='font-weight:bold; color:#dc3545;'>$0</span></div> </div> <br> <div id='nuevo-pago-section-${proyectoId}'> <h5>Nuevo Pago</h5> <form id='addPagoForm' onsubmit='event.preventDefault(); addPago(window.currentProject.id);' style='margin-bottom: 20px;'> <input type='hidden' name='proyecto_id' value='${proyectoId}'> <div class='row g-3 mb-3'> <div class='col-md-4'> <label class="form-label">Fecha:</label> <input type='date' name='fecha' id='pago-fecha' class='form-control' required> </div> <div class='col-md-4'> <label class='form-label'>Monto:</label> <input type='number' name='monto' id='pago-monto' class='form-control' min='0' step='0.01' required> </div> <div class='col-md-4'> <label class='form-label'>Descripción:</label> <input type='text' name='descripcion' class='form-control'> </div> </div> <button type='submit' class='btn btn-primary w-100'>Agregar Pago</button> </form> </div> `; // Establecer fecha actual por defecto const today = new Date().toISOString().split('T')[0]; document.getElementById('pago-fecha').value = today; // Cargar pagos existentes fetch(`router.php?action=getPagosJson&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(pagos => { renderPagos(pagos, proyectoId); // Calcular y establecer monto pendiente por defecto después de cargar pagos y detalles del proyecto // Necesitamos el total del proyecto, que se carga con loadFacturacion loadFacturacionForPagos(proyectoId, pagos); // Cargar detalles de factura para calcular pendiente }) .catch(error => console.error('Error loading pagos:', error)); } // Función auxiliar para renderizar la lista de pagos function renderPagos(pagos, proyectoId) { const listaContainer = document.getElementById(`pagos-lista-${proyectoId}`); if (!listaContainer) return; if (pagos.length === 0) { listaContainer.innerHTML = '<div class="no-acciones">No hay pagos registrados</div>'; // Reutilizamos la clase no-acciones return; } let html = '<table class="table table-striped"><thead><tr><th>Fecha</th><th>Monto</th><th>Descripción</th><th></th></tr></thead><tbody>'; let totalAbonado = 0; pagos.forEach(pago => { // Formatear fecha para mostrar (opcional, depende del formato de la BD) const fechaPago = new Date(pago.fecha).toLocaleDateString(); // Formato local básico totalAbonado += parseFloat(pago.monto); html += `<tr> <td>${fechaPago}</td> <td>$${parseFloat(pago.monto).toFixed(2)}</td> <td>${pago.descripcion || '-'}</td> <td> <button type="button" class="btn-icon-round delete" onclick="deletePago(${pago.id}, ${proyectoId})"><i class="fa fa-trash"></i></button> </td> </tr>`; }); html += '</tbody></table>'; listaContainer.innerHTML = html; // Actualizar total abonado en la interfaz document.getElementById(`total-abonado-${proyectoId}`).textContent = '$ ' + totalAbonado.toFixed(2); // El cálculo del pendiente se hará después de cargar los detalles de factura con loadFacturacionForPagos } // Función auxiliar para cargar los detalles de factura y calcular el pendiente function loadFacturacionForPagos(proyectoId, pagos) { fetch(`router.php?action=getProjectDetails&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(data => { const totalProyecto = parseFloat(data.total_proyecto || 0); let totalAbonado = 0; pagos.forEach(pago => { totalAbonado += parseFloat(pago.monto); }); const saldoPendiente = totalProyecto - totalAbonado; // Actualizar saldo pendiente en la interfaz document.getElementById(`total-pendiente-${proyectoId}`).textContent = '$ ' + saldoPendiente.toFixed(2); // Establecer monto pendiente por defecto en el formulario de pago document.getElementById('pago-monto').value = saldoPendiente > 0 ? saldoPendiente.toFixed(2) : '0.00'; // Ocultar o mostrar la sección de nuevo pago según el saldo pendiente const nuevoPagoSection = document.getElementById(`nuevo-pago-section-${proyectoId}`); if (nuevoPagoSection) { nuevoPagoSection.style.display = saldoPendiente > 0 ? 'block' : 'none'; } }) .catch(error => { console.error('Error loading project details for pagos:', error); document.getElementById(`total-pendiente-${proyectoId}`).textContent = '$ Error'; document.getElementById('pago-monto').value = '0.00'; }); } // Función para añadir un nuevo pago function addPago(proyectoId) { const form = document.getElementById('addPagoForm'); const formData = new FormData(form); fetch('router.php?action=addPagoAjax', { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.success) { Swal.fire({ icon: 'success', title: '¡Pago agregado!', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', showConfirmButton: false, timer: 2000 }); // Recargar la lista de pagos loadPagos(proyectoId); // Limpiar formulario (excepto la fecha) form.reset(); const today = new Date().toISOString().split('T')[0]; document.getElementById('pago-fecha').value = today; // Volver a calcular y establecer el monto pendiente por defecto // Esto se hará automáticamente con la llamada a loadFacturacionForPagos dentro de loadPagos } else { Swal.fire({ icon: 'error', title: 'Error al agregar pago', text: result.message || 'No se pudo agregar el pago.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); } }) .catch(error => { console.error('Error adding pago:', error); Swal.fire({ icon: 'error', title: 'Error de conexión', text: 'Ocurrió un error al intentar agregar el pago.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); }); } // Función para eliminar un pago function deletePago(pagoId, proyectoId) { Swal.fire({ title: '¿Eliminar pago?', text: 'Esta acción no se puede deshacer', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#23252b', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', background: '#000', color: '#fff', zIndex: 9999 }).then((result) => { if (result.isConfirmed) { fetch(`router.php?action=deletePagoAjax&id=${pagoId}`, { method: 'POST', }) .then(response => response.json()) .then(result => { if (result.success) { Swal.fire('¡Eliminado!', 'El pago ha sido eliminado.', 'success'); loadPagos(proyectoId); } else { Swal.fire('Error!', result.message || 'Hubo un error al eliminar el pago.', 'error'); } }) .catch(error => { console.error('Error deleting pago:', error); Swal.fire('Error de conexión!', 'No se pudo comunicar con el servidor para eliminar el pago.', 'error'); }); } }); } // Función JavaScript para obtener la clase CSS del badge de estado function getStatusBadgeClass(status) { switch (status) { case 'Negociacion': return 'badge-negociacion'; case 'Planificacion': return 'badge-planificacion'; case 'Creacion': return 'badge-creacion'; case 'Revision': return 'badge-revision'; case 'Publicacion': return 'badge-publicacion'; default: return 'badge-secondary'; } } function loadProjectDetalles(projectId) { // Cargar detalles actuales del proyecto y mostrarlos en el modal fetch(`router.php?action=getProjectDetails&proyecto_id=${projectId}`) .then(response => response.json()) .then(data => { if (data && data.detalles && Array.isArray(data.detalles)) { detallesTemp = data.detalles.map(det => ({ producto_id: det.producto_id, producto_nombre: det.producto_nombre, cantidad: parseFloat(det.cantidad), precio_unitario: parseFloat(det.precio), subtotal: parseFloat(det.precio) * parseFloat(det.cantidad) })); renderDetalles(); calcularTotal(); } }) .catch(error => console.error('Error cargando detalles del proyecto:', error)); } function saveProjectDetalles(projectId) { // Guardar los productos del proyecto vía AJAX if (!detallesTemp.length) { Swal.fire({ icon: 'warning', title: 'No hay productos para guardar', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', }); return; } const descuento = parseFloat(document.getElementById('descuento').value) || 0; fetch('router.php?action=saveProjectDetalles', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proyecto_id: projectId, detalles: detallesTemp, descuento: descuento }) }) .then(response => response.json()) .then(result => { if (result.success) { Swal.fire({ icon: 'success', title: 'Productos guardados', background: '#000', color: '#fff', confirmButtonColor: '#00bfff', timer: 2000, showConfirmButton: false }); closeProductosModal(); // Actualizar la facturación en la sidebar si está abierta if (window.currentProject) { loadFacturacion(window.currentProject.id); } } else { Swal.fire({ icon: 'error', title: 'Error al guardar', text: result.message || 'No se pudieron guardar los productos.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); } }) .catch(error => { console.error('Error guardando productos:', error); Swal.fire({ icon: 'error', title: 'Error de conexión', text: 'Ocurrió un error al intentar guardar los productos.', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); }); } let currentContenidoIndex = null; let currentContenidosList = []; function loadContenidos(proyectoId) { fetch(`router.php?action=getContenidos&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(contenidos => { let html = '<table class="table table-striped"><thead><tr><th>Descripción</th><th>Archivo</th><th>URL</th><th></th></tr></thead><tbody>'; contenidos.forEach((c, idx) => { html += `<tr> <td>${c.contenido}</td> <td>${c.archivo ? `<a href="#" onclick="previewContenidoArchivo('uploads/contenidos/${c.archivo}', '${c.archivo}', '${c.url ? encodeURIComponent(c.url) : ''}', ${idx});return false;">Ver archivo</a>` : '-'}</td> <td>${c.url ? `<a href="#" onclick=\"previewContenidoArchivo('', '', '${encodeURIComponent(c.url)}', ${idx});return false;\">Enlace</a>` : '-'}</td> <td><button type="button" class="btn-icon-round delete" onclick="deleteContenido(${c.id}, ${proyectoId})"><i class="fa fa-trash"></i></button></td> </tr>`; }); html += '</tbody></table>'; const lista = document.getElementById('contenidos-lista'); if (lista) { lista.innerHTML = html; } else { console.error('No se encontró el div contenidos-lista'); } // Guardar la lista actual para navegación currentContenidosList = contenidos; }); } function deleteContenido(id, proyectoId) { Swal.fire({ title: '¿Eliminar contenido?', text: 'Esta acción no se puede deshacer', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#23252b', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', background: '#000', color: '#fff', }).then((result) => { if (result.isConfirmed) { fetch('router.php?action=deleteContenido', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'id=' + id }).then(() => loadContenidos(proyectoId)); } }); } document.getElementById('contenidoForm').onsubmit = function(e) { e.preventDefault(); const formData = new FormData(this); fetch('router.php?action=addContenido', { method: 'POST', body: formData }).then(() => { loadContenidos(formData.get('proyecto_id')); this.reset(); }); }; function getYouTubeId(url) { // Soporta watch?v=, youtu.be/, shorts/ let id = ''; let match = url.match(/[?&]v=([^&#]+)/); if (match) return match[1]; match = url.match(/youtu\.be\/([^?&#]+)/); if (match) return match[1]; match = url.match(/youtube\.com\/shorts\/([^?&#]+)/); if (match) return match[1]; return ''; } // Agregar estilos para las flechas de navegación del modal de contenidos const style = document.createElement('style'); style.innerHTML = ` .contenido-arrow-btn { border: 0.2px solid #00bfff; color: #00bfff; border-radius: 10px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 1.7em; cursor: pointer; transition: box-shadow 0.2s, border-color 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.10); outline: none; margin: 0 8px; } .contenido-arrow-btn:hover { border-color: #0099e5; background: #0099e5; color: #fff; } `; document.head.appendChild(style); function previewContenidoArchivo(ruta, nombre, url = null, idx = null) { let ext = ''; if (nombre) { ext = nombre.split('.').pop().toLowerCase(); } let html = ''; // Soporte mejorado para todos los formatos de YouTube if (url && (url.includes('youtube.com') || url.includes('youtu.be'))) { let videoId = getYouTubeId(decodeURIComponent(url)); if (videoId) { html = `<iframe width='100%' height='400' src='https://www.youtube.com/embed/${videoId}?rel=0&loop=1&playlist=${videoId}' frameborder='0' allowfullscreen style='border-radius:10px;'></iframe>`; } else { html = `<div style='color:#888;'>Enlace de YouTube no válido.</div>`; } } else if(['jpg','jpeg','png','gif','webp','bmp'].includes(ext)) { html = `<img src='${ruta}' alt='Imagen' style='max-width:100%;max-height:400px;border-radius:10px;'>`; } else if(['mp4','webm','ogg'].includes(ext)) { html = `<video src='${ruta}' controls style='max-width:100%;max-height:400px;border-radius:10px;'></video>`; } else if(['mp3','wav','aac','ogg'].includes(ext)) { html = `<audio src='${ruta}' controls style='width:100%;margin-top:20px;'></audio>`; } else if(['pdf'].includes(ext)) { html = `<embed src='${ruta}' type='application/pdf' width='100%' height='400px' style='border-radius:10px;' />`; } else { html = `<div style='color:#888;'>No se puede previsualizar este tipo de archivo.</div>`; } // Flechas de navegación if (currentContenidosList && Array.isArray(currentContenidosList) && currentContenidosList.length > 1 && idx !== null) { currentContenidoIndex = idx; html = `<div style='display:flex;align-items:center;justify-content:space-between;'> <button onclick='navigateContenido(-1)' class='contenido-arrow-btn' title='Anterior'><</button> <div style='flex:1;text-align:center;'>${html}</div> <button onclick='navigateContenido(1)' class='contenido-arrow-btn' title='Siguiente'>></button> </div>`; } document.getElementById('contenido-preview-body').innerHTML = html; document.getElementById('contenidoPreviewModal').style.display = 'flex'; } function navigateContenido(direction) { if (currentContenidosList && currentContenidoIndex !== null) { let newIndex = currentContenidoIndex + direction; if (newIndex < 0) newIndex = currentContenidosList.length - 1; if (newIndex >= currentContenidosList.length) newIndex = 0; const c = currentContenidosList[newIndex]; previewContenidoArchivo(c.archivo ? 'uploads/contenidos/' + c.archivo : '', c.archivo || '', c.url ? encodeURIComponent(c.url) : '', newIndex); } } function closeContenidoPreview() { document.getElementById('contenidoPreviewModal').style.display = 'none'; document.getElementById('contenido-preview-body').innerHTML = ''; } function formatDateOnly(dateString) { if (!dateString) return '-'; const date = new Date(dateString); if (isNaN(date.getTime())) return '-'; const days = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado']; const dayOfWeek = date.getDay(); const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear(); return days[dayOfWeek] + ' ' + day + '-' + month + '-' + year; } function formatHourRange(inicio, fin) { if (!inicio) return ''; const d1 = new Date(inicio); let h1 = d1.getHours(); let m1 = d1.getMinutes().toString().padStart(2, '0'); let ampm1 = h1 >= 12 ? 'pm' : 'am'; h1 = h1 % 12; h1 = h1 ? h1 : 12; let hora1 = `${h1}:${m1}${ampm1}`; if (!fin) return hora1; const d2 = new Date(fin); let h2 = d2.getHours(); let m2 = d2.getMinutes().toString().padStart(2, '0'); let ampm2 = h2 >= 12 ? 'pm' : 'am'; h2 = h2 % 12; h2 = h2 ? h2 : 12; let hora2 = `${h2}:${m2}${ampm2}`; // Si es el mismo día, solo mostrar horas if (d1.toDateString() === d2.toDateString()) { return `${hora1} – ${hora2}`; } else { // Si son días distintos, mostrar ambos días y horas return `${formatDateOnly(inicio)}, ${hora1} – ${formatDateOnly(fin)}, ${hora2}`; } } // Agregar función para mostrar el selector de estado function showStatusSelector(accionId, currentStatus) { const statusOptions = [ { value: 'Pendiente', label: 'Pendiente', color: '#dc3545', icon: 'fa-clock' }, { value: 'En Progreso', label: 'En Progreso', color: '#ffc107', icon: 'fa-gear' }, { value: 'Lograda', label: 'Lograda', color: '#28a745', icon: 'fa-check' } ]; let html = '<div style=\'display:flex;flex-direction:column;gap:8px;\'>'; statusOptions.forEach(opt => { html += `<button onclick=\"changeAccionStatus(${accionId}, '${opt.value}')\" style=\"display:flex;align-items:center;gap:8px;background:${opt.color};color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:1em;cursor:pointer;\"><i class=\"fa ${opt.icon}\"></i> ${opt.label}</button>`; }); html += '</div>'; Swal.fire({ title: 'Cambiar estado', html: html, showConfirmButton: false, background: '#23252b', color: '#fff', customClass: { popup: 'swal2-modal-status-selector' } }); } // Cambiar el estado vía AJAX function changeAccionStatus(accionId, newStatus) { fetch('router.php?action=changeAccionStatus', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `id=${accionId}&status=${encodeURIComponent(newStatus)}` }) .then(response => response.json()) .then(result => { if (result.success) { Swal.close(); if (window.currentProject) loadAcciones(window.currentProject.id); } else { Swal.fire({ icon: 'error', title: 'Error', text: result.message || 'No se pudo cambiar el estado', background: '#000', color: '#fff', confirmButtonColor: '#00bfff' }); } }); } // Agregar funciones para mostrar/ocultar el dropdown y cerrar al hacer clic fuera function toggleStatusDropdown(event, accionId, currentStatus) { event.stopPropagation(); // Cerrar otros dropdowns document.querySelectorAll('.status-dropdown').forEach(el => el.style.display = 'none'); const dropdown = document.getElementById('status-dropdown-' + accionId); if (dropdown) { dropdown.style.display = (dropdown.style.display === 'block') ? 'none' : 'block'; } // Cerrar al hacer clic fuera document.addEventListener('click', closeAllStatusDropdowns); } function closeAllStatusDropdowns(e) { document.querySelectorAll('.status-dropdown').forEach(el => el.style.display = 'none'); document.removeEventListener('click', closeAllStatusDropdowns); } // Agregar función vacía para openProjectCalendar (puedes implementar la lógica luego) function openProjectCalendar(proyectoId) { const sidebar = document.getElementById('projectSidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.dataset.locked = '1'; } document.getElementById('projectCalendarModal').style.display = 'flex'; loadProjectCalendar(proyectoId); } function closeProjectCalendarModal() { document.getElementById('projectCalendarModal').style.display = 'none'; } let calendarMonthOffset = 0; function changeCalendarMonth(offset) { calendarMonthOffset += offset; if (window.currentProject) loadProjectCalendar(window.currentProject.id); } function loadProjectCalendar(proyectoId) { fetch(`router.php?action=getAcciones&proyecto_id=${proyectoId}`) .then(response => response.json()) .then(acciones => { renderProjectCalendar(acciones); }); } function renderProjectCalendar(acciones) { const now = new Date(); const baseDate = new Date(now.getFullYear(), now.getMonth() + calendarMonthOffset, 1); const year = baseDate.getFullYear(); const month = baseDate.getMonth(); const monthLabel = baseDate.toLocaleString('es-ES', { month: 'long', year: 'numeric' }); document.getElementById('calendar-month-label').textContent = monthLabel.charAt(0).toUpperCase() + monthLabel.slice(1); // Generar días del mes const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); let html = '<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;">'; const weekDays = ['Dom','Lun','Mar','Mié','Jue','Vie','Sáb']; weekDays.forEach(d => html += `<div style='text-align:center;font-weight:600;color:#00bfff;padding:6px 0;'>${d}</div>`); let dayCell = 0; for(let i=0;i<firstDay;i++) { html += `<div></div>`; dayCell++; } for(let d=1;d<=daysInMonth;d++,dayCell++) { const dateStr = `${year}-${(month+1).toString().padStart(2,'0')}-${d.toString().padStart(2,'0')}`; const accionesDia = acciones.filter(a => a.inicio && a.inicio.startsWith(dateStr)); html += `<div style='background:#fff;border-radius:8px;min-height:60px;max-height:120px;padding:4px 2px 2px 2px;box-shadow:0 1px 4px rgba(0,0,0,0.04);position:relative;overflow-y:auto;display:flex;flex-direction:column;'>`; html += `<div style='text-align:right;font-size:0.95em;color:#888;font-weight:600;'>${d}</div>`; const maxToShow = 3; accionesDia.slice(0,maxToShow).forEach(a => { let color = '#bdbdbd'; if(a.status==='Pendiente') color='#dc3545'; else if(a.status==='En Progreso') color='#ffc107'; else if(a.status==='Lograda') color='#28a745'; html += `<div style='background:${color};color:#fff;border-radius:6px;padding:2px 6px;font-size:0.95em;margin:2px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:98%;cursor:pointer;' title='${a.accion}' onclick='handleCalendarActionClick(${a.id})'>${a.accion}</div>`; }); if (accionesDia.length > maxToShow) { html += `<div style='color:#00bfff;font-size:0.9em;margin-top:2px;cursor:pointer;' title='Ver todas las acciones del día'>+${accionesDia.length-maxToShow} más</div>`; } html += `</div>`; } html += '</div>'; document.getElementById('calendar-grid').innerHTML = html; } function handleCalendarActionClick(accionId) { if (window.currentProject) { showSidebarTab('info'); window.pendingAccionScroll = accionId; const card = document.getElementById(`accion-card-${accionId}`); if (card) { scrollToAccion(accionId); delete window.pendingAccionScroll; } else { loadAcciones(window.currentProject.id); } } closeProjectCalendarModal(); editAccion(accionId); } function scrollToAccion(accionId) { const sidebarTab = document.querySelector(`#acciones-lista-${window.currentProject?.id}`); const card = document.getElementById(`accion-card-${accionId}`); if (card) { card.classList.add('accion-highlight'); card.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { card.classList.remove('accion-highlight'); }, 1600); } } // Autocompletar el campo de acción al seleccionar un producto function initContractEditors() { if (!window.tinymce) { setTimeout(initContractEditors, 150); return; } if (window.contractEditorsInit) { return; } window.contractEditorsInit = true; window.contractEditors = window.contractEditors || {}; tinymce.init({ selector: '#project-condiciones, #editar-project-condiciones', height: 260, menubar: false, plugins: 'lists link', toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright | bullist numlist | outdent indent | removeformat', branding: false, convert_urls: false, setup: function(editor) { editor.on('init', function() { if (editor.id === 'project-condiciones') { window.contractEditors.create = editor; if (!editor.getContent().trim()) { editor.setContent(defaultContractHtml); editor.undoManager.clear(); } } else if (editor.id === 'editar-project-condiciones') { window.contractEditors.edit = editor; } }); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initContractEditors); } else { initContractEditors(); } // Lógica para mostrar el campo de cantidad y autocompletar el nombre de la acción document.addEventListener('DOMContentLoaded', function() { const select = document.getElementById('accion-producto-select'); const cantidadGroup = document.getElementById('accion-producto-cantidad-group'); const cantidadInput = document.getElementById('accion-producto-cantidad'); const inicioInput = document.getElementById('accion-inicio'); const finInput = document.getElementById('accion-fin'); if (select && cantidadGroup && cantidadInput && inicioInput && finInput) { function actualizarFinPorCantidad() { const productoId = select.value; let productoNombre = ''; let max = 1; for (const nombre in productosProyecto) { if (productosProyecto[nombre].producto_id == productoId) { productoNombre = nombre; max = productosProyecto[nombre].cantidad; break; } } const cantidad = parseInt(cantidadInput.value) || 1; const inicio = inicioInput.value; if (productoNombre && cantidad && inicio) { // inicio formato: 'YYYY-MM-DDTHH:MM' const inicioDate = new Date(inicio); if (!isNaN(inicioDate.getTime())) { // Sumar cantidad de horas const finDate = new Date(inicioDate.getTime() + cantidad * 60 * 60 * 1000); // Formatear a 'YYYY-MM-DDTHH:MM' const yyyy = finDate.getFullYear(); const mm = String(finDate.getMonth() + 1).padStart(2, '0'); const dd = String(finDate.getDate()).padStart(2, '0'); const hh = String(finDate.getHours()).padStart(2, '0'); const min = String(finDate.getMinutes()).padStart(2, '0'); finInput.value = `${yyyy}-${mm}-${dd}T${hh}:${min}`; } } } select.addEventListener('change', function() { const productoId = this.value; let productoNombre = ''; let max = 1; for (const nombre in productosProyecto) { if (productosProyecto[nombre].producto_id == productoId) { productoNombre = nombre; max = productosProyecto[nombre].cantidad; break; } } if (productoId && productoNombre && max > 0) { cantidadGroup.style.display = ''; cantidadInput.max = max; cantidadInput.value = 1; document.getElementById('accion-nombre').value = `${productoNombre} (1 hora)`; actualizarFinPorCantidad(); } else { cantidadGroup.style.display = 'none'; document.getElementById('accion-nombre').value = ''; } }); cantidadInput.addEventListener('input', function() { const productoId = select.value; let productoNombre = ''; for (const nombre in productosProyecto) { if (productosProyecto[nombre].producto_id == productoId) { productoNombre = nombre; break; } } const cantidad = this.value; if (productoId && productoNombre && cantidad) { let label = `${productoNombre} (${cantidad} ${cantidad == 1 ? 'hora' : 'horas'})`; document.getElementById('accion-nombre').value = label; actualizarFinPorCantidad(); } }); inicioInput.addEventListener('input', function() { actualizarFinPorCantidad(); }); } }); // Agregar estilos CSS para los nuevos badges (al final del archivo o en el bloque <style>) const styleBadges = document.createElement('style'); styleBadges.innerHTML = ` .badge-planificacion { background: #6b00e5 !important; color: #fff !important; } .badge-creacion { background: #ff9800 !important; color: #fff !important; } .badge-revision { background: #00bfff !important; color: #fff !important; } .badge-publicacion { background: #28a745 !important; color: #fff !important; } .badge-secondary { background: #bdbdbd !important; color: #fff !important; } `; document.head.appendChild(styleBadges); // 1. Sidebar: hacer la badge clickeable y mostrar un dropdown tipo ciclo/línea de tiempo justo debajo // Reemplazar showProjectStatusSelector y changeProjectStatus por un dropdown visual // --- Agregar HTML dinámico del dropdown debajo de la badge --- function showProjectStatusDropdown() { const existing = document.getElementById('project-status-dropdown'); if (existing) existing.remove(); const badge = document.getElementById('sidebar-project-status-badge'); if (!badge) return; const statusOptions = [ { value: 'Negociacion', label: 'Negociación', color: '#ff5c5c' }, { value: 'Planificacion', label: 'Planificación', color: '#6b00e5' }, { value: 'Creacion', label: 'Creación', color: '#ff9800' }, { value: 'Revision', label: 'Revisión', color: '#00bfff' }, { value: 'Publicacion', label: 'Publicación', color: '#28a745' } ]; // Determinar el índice del estado actual const currentIdx = statusOptions.findIndex(opt => window.currentProject && window.currentProject.status === opt.value); let html = `<div id='project-status-dropdown' style='position:absolute;left:50%;transform:translateX(-50%);top:calc(100% + 8px);z-index:9999;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.13);padding:14px 18px;min-width:260px;display:flex;flex-direction:column;align-items:center;'>`; html += `<div style='display:flex;align-items:center;gap:0;margin-bottom:8px;'>`; statusOptions.forEach((opt, idx) => { // Sombrear en gris si el paso no se ha logrado aún let isAchieved = idx <= currentIdx; let btnStyle = `background:${isAchieved ? opt.color : '#e0e0e0'};color:${isAchieved ? '#fff' : '#888'};border:none;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:1em;font-weight:bold;box-shadow:0 1px 4px rgba(0,0,0,0.10);margin-bottom:2px;${window.currentProject && window.currentProject.status===opt.value ? 'outline:3px solid #23252b;' : ''}`; html += `<div style='display:flex;flex-direction:column;align-items:center;'>`; html += `<button onclick=\"changeProjectStatus('${opt.value}')\" style=\"${btnStyle}\">${idx+1}</button>`; html += `<span style='font-size:0.92em;color:${isAchieved ? '#222' : '#bbb'};margin-top:2px;'>${opt.label}</span>`; html += `</div>`; if (idx < statusOptions.length-1) { html += `<div style='width:32px;height:2px;background:${isAchieved ? opt.color : '#e0e0e0'};margin:0 2px;align-self:center;'></div>`; } }); html += `</div>`; html += `</div>`; badge.insertAdjacentHTML('afterend', html); setTimeout(() => { document.addEventListener('click', closeProjectStatusDropdown, { once: true }); }, 10); } function closeProjectStatusDropdown(e) { const dropdown = document.getElementById('project-status-dropdown'); if (dropdown) dropdown.remove(); } // Reemplazar changeProjectStatus para cerrar el dropdown y actualizar visualmente function changeProjectStatus(newStatus) { if (!window.currentProject) return; const projectId = window.currentProject.id; fetch('router.php?action=changeProjectStatus', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `id=${projectId}&status=${encodeURIComponent(newStatus)}` }) .then(response => response.json()) .then(result => { if (result.success) { closeProjectStatusDropdown(); window.currentProject.status = newStatus; updateSidebarHeader(window.currentProject); // Actualizar la card en el listado principal const projectCard = document.querySelector(`.project-card-full[data-project-id="${projectId}"]`); if (projectCard) { const statusBadgeElement = projectCard.querySelector('.badge'); if (statusBadgeElement) { statusBadgeElement.textContent = ' ' + newStatus + ' '; statusBadgeElement.className = 'badge ' + getStatusBadgeClass(newStatus); } } } else { alert('No se pudo cambiar el estado'); } }); } document.addEventListener('click', function(event) { const sidebar = document.getElementById('projectSidebar'); if (!sidebar) return; if (sidebar.classList.contains('open')) { // Si el clic fue dentro de la sidebar, no cerrar if (sidebar.contains(event.target)) return; // Si el clic fue en una card de proyecto, no cerrar (deja que la lógica de openProjectSidebar actúe) if (event.target.closest('.user-card.project-card-full')) return; // Si el clic fue en el botón para abrir la sidebar, tampoco cerrar closeProjectSidebar(); } }); </script>
Coded With 💗 by
0x6ick