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
/
aircan.me
/
public_html
/
francisco
/
Viewing: panel.php
<?php session_start(); if (!isset($_SESSION['usuario'])) { header('Location: login.html'); exit; } $nombre = $_SESSION['nombre']; $rol = $_SESSION['rol']; ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Panel Administrativo - Contador Francisco</title> <link rel="icon" type="image/png" href="images/logo.png"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script> <style> :root { --primary: #0f172a; --secondary: #60a5fa; --accent: #22d3ee; --success: #22c55e; --light: #e5e7eb; --dark: #0b1220; --sidebar-width: 240px; --header-height: 80px; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: radial-gradient(1200px circle at 10% 0%, #0b1220 0%, #0f172a 40%, #0b1220 100%); color: white; overflow-x: hidden; min-height: 100vh; } /* Layout Principal */ .admin-container { display: flex; min-height: 100vh; } /* Sidebar Estilizado */ .admin-sidebar { width: var(--sidebar-width); background: linear-gradient(180deg, var(--primary) 0%, var(--dark) 100%); color: white; position: fixed; height: 100vh; overflow-y: auto; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 1000; box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); border-right: 1px solid rgba(255, 255, 255, 0.1); } .sidebar-header { padding: 20px 16px; text-align: center; border-bottom: 1px solid rgba(255, 255, 255, 0.08); background: transparent; } .admin-logo img { width: 110px; height: auto; display: block; margin: 0 auto 12px auto; } .admin-logo div { font-size: 1.15rem; font-weight: 800; text-align: center; letter-spacing: 1.2px; color: #fff; text-shadow: 0 1px 4px rgba(15,23,42,0.35); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: transparent; padding: 4px 0; } .sidebar-header h2 { font-size: 1.6rem; margin-top: 10px; font-weight: 700; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .admin-menu { padding: 16px 0; } .menu-item { padding: 12px 18px; display: flex; align-items: center; cursor: pointer; transition: all 0.3s; border-left: 3px solid transparent; margin: 4px 8px; border-radius: 6px; } .menu-item:hover { background: rgba(255, 255, 255, 0.08); border-left: 3px solid var(--secondary); transform: translateX(4px); } .menu-item.active { background: rgba(52, 152, 219, 0.15); border-left: 3px solid var(--accent); } .menu-item i { font-size: 1.2rem; margin-right: 12px; width: 26px; text-align: center; color: var(--secondary); transition: all 0.3s; } .menu-item:hover i { color: var(--accent); transform: scale(1.15); } .menu-item span { font-size: 1rem; font-weight: 500; } /* Main Content */ .admin-main { flex: 1; margin-left: var(--sidebar-width); transition: all 0.4s; } /* Header con efecto de cristal */ .admin-header { background: rgba(255, 255, 255, 0.08); backdrop-filter: blur(10px); height: var(--header-height); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; box-shadow: 0 2px 15px rgba(0, 0, 0, 0.2); position: sticky; top: 0; z-index: 100; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .header-left { display: flex; align-items: center; } .toggle-sidebar { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; position: relative; display: none; } @media (max-width: 992px) { .toggle-sidebar { display: flex; } } .toggle-sidebar .hamburger { width: 28px; height: 22px; position: relative; display: inline-block; cursor: pointer; z-index: 1100; } .toggle-sidebar .hamburger span { display: block; position: absolute; height: 4px; width: 100%; background: var(--secondary); border-radius: 2px; opacity: 1; left: 0; transition: all 0.3s cubic-bezier(.68,-0.55,.27,1.55); } .toggle-sidebar .hamburger span:nth-child(1) { top: 0; } .toggle-sidebar .hamburger span:nth-child(2) { top: 9px; } .toggle-sidebar .hamburger span:nth-child(3) { top: 18px; } .toggle-sidebar.active .hamburger span:nth-child(1) { top: 9px; transform: rotate(45deg); } .toggle-sidebar.active .hamburger span:nth-child(2) { opacity: 0; } .toggle-sidebar.active .hamburger span:nth-child(3) { top: 9px; transform: rotate(-45deg); } .header-title { font-size: 1.7rem; font-weight: 700; color: white; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .user-menu { display: flex; align-items: center; } .user-info { text-align: right; margin-right: 20px; } .user-name { font-weight: 700; color: white; font-size: 1.1rem; } .user-role { font-size: 0.9rem; color: var(--secondary); margin-top: 3px; } .user-avatar { width: 55px; height: 55px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; font-size: 1.6rem; cursor: pointer; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 3px solid rgba(255, 255, 255, 0.2); position: relative; overflow: hidden; } .user-avatar::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); transform: rotate(45deg); transition: all 0.6s; opacity: 0; } .user-avatar:hover::before { opacity: 1; transform: rotate(45deg) translate(50%, 50%); } .user-avatar:hover { transform: scale(1.15) rotate(15deg); box-shadow: 0 15px 35px var(--avatar-color, #667eea); border-color: rgba(255, 255, 255, 0.4); } .user-avatar:active { transform: scale(0.95); } /* Content */ .admin-content { padding: 35px; } .content-header { margin-bottom: 35px; display: flex; justify-content: space-between; align-items: center; } .content-title { font-size: 2.2rem; color: white; font-weight: 800; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); position: relative; padding-bottom: 15px; } .content-title::after { content: ''; position: absolute; width: 80px; height: 4px; background: linear-gradient(90deg, var(--secondary), var(--accent)); bottom: 0; left: 0; border-radius: 2px; } .btn { padding: 14px 30px; border: none; border-radius: 10px; font-weight: 700; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 12px; font-size: 1.1rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .btn-primary { background: linear-gradient(135deg, var(--secondary) 0%, var(--accent) 100%); color: white; } .btn-primary:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(52, 152, 219, 0.4); } .btn-success { background: linear-gradient(135deg, var(--success) 0%, #2ecc71 100%); color: white; } .btn-success:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(46, 204, 113, 0.4); } /* Tarjetas de estadísticas */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 25px; margin-bottom: 40px; } .stat-card { background: rgba(255, 255, 255, 0.08); backdrop-filter: blur(10px); border-radius: 18px; padding: 30px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); display: flex; align-items: center; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 1px solid rgba(255, 255, 255, 0.1); } .stat-card:hover { transform: translateY(-10px); background: rgba(255, 255, 255, 0.12); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3); } .stat-icon { width: 80px; height: 80px; border-radius: 20px; display: flex; align-items: center; justify-content: center; font-size: 2.5rem; margin-right: 25px; background: rgba(52, 152, 219, 0.15); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .stat-icon i { color: var(--secondary); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .stat-info h3 { font-size: 2.3rem; margin-bottom: 8px; color: white; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .stat-info p { color: var(--secondary); font-size: 1.1rem; font-weight: 500; } /* Formularios de gestión */ .form-container { background: rgba(255, 255, 255, 0.08); backdrop-filter: blur(10px); border-radius: 20px; padding: 35px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); margin-bottom: 40px; border: 1px solid rgba(255, 255, 255, 0.1); } .table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .form-header { margin-bottom: 30px; padding-bottom: 15px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .form-header h3 { font-size: 1.8rem; color: white; font-weight: 700; } .form-group { margin-bottom: 28px; } .form-group label { display: block; margin-bottom: 12px; font-weight: 600; color: var(--secondary); font-size: 1.1rem; } .form-control { width: 100%; padding: 16px 20px; border: none; background: rgba(255, 255, 255, 0.08); border-radius: 12px; font-size: 1.1rem; transition: all 0.3s; color: white; border: 1px solid transparent; } .form-control:focus { border-color: var(--secondary); outline: none; background: rgba(255, 255, 255, 0.12); box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.2); } textarea.form-control { min-height: 180px; resize: vertical; } .form-row { display: flex; gap: 25px; margin-bottom: 28px; } .form-col { flex: 1; } .image-preview { width: 100%; height: 250px; border: 2px dashed rgba(255, 255, 255, 0.2); border-radius: 15px; display: flex; align-items: center; justify-content: center; overflow: hidden; margin-top: 15px; background: rgba(0, 0, 0, 0.1); transition: all 0.3s; } .image-preview:hover { border-color: var(--secondary); } .image-preview img { max-width: 100%; max-height: 100%; object-fit: contain; transition: all 0.4s; } .image-preview img:hover { transform: scale(1.05); } .form-actions { display: flex; gap: 20px; margin-top: 30px; } /* Tabla de elementos */ .items-table { width: 100%; border-collapse: separate; border-spacing: 0; margin-top: 25px; background: rgba(255, 255, 255, 0.05); border-radius: 18px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.1); } .items-table th { background: linear-gradient(135deg, var(--primary) 0%, var(--dark) 100%); padding: 20px; text-align: left; font-weight: 700; color: var(--secondary); font-size: 1.1rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .items-table td { padding: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.9); } .items-table tr:last-child td { border-bottom: none; } .items-table tr:hover td { background: rgba(255, 255, 255, 0.03); } .action-btn { padding: 10px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; } .acciones-inline { display: flex; gap: 10px; } .moneda-actions { display: flex; gap: 8px; flex-wrap: wrap; } .edit-btn { background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: white; } .delete-btn { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; } .invoice-btn { background: linear-gradient(135deg, #7c3aed 0%, #4c1d95 100%); color: #fdf4ff; } .action-btn:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } /* Efectos de partículas para el fondo */ .particles { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; overflow: hidden; } .particle { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.1); animation: float 15s infinite linear; } @keyframes float { 0% { transform: translateY(0) translateX(0) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { transform: translateY(-100vh) translateX(100px) rotate(360deg); opacity: 0; } } /* Responsive */ @media (max-width: 992px) { .admin-sidebar { transform: translateX(-100%); transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .admin-sidebar.active { transform: translateX(0); } .admin-main { margin-left: 0; } .sidebar-overlay { display: block; } } .sidebar-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(44,62,80,0.55); z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .sidebar-overlay.active { opacity: 1; pointer-events: all; } @media (max-width: 768px) { .admin-header { padding: 0 20px; } .user-info { display: none; } .admin-content { padding: 25px; } .form-row { flex-direction: column; gap: 0; } } /* Eliminar ocultamiento de la tabla en móvil y mejorar visualización */ @media (max-width: 700px) { .items-table { display: none; } .items-table.monedas-table { display: block; border: none; background: transparent; box-shadow: none; padding: 0; } .items-table.monedas-table thead { display: none; } .items-table.monedas-table tbody { display: flex; flex-direction: column; gap: 14px; } .items-table.monedas-table tr { display: flex; flex-direction: column; gap: 10px; background: rgba(15, 23, 42, 0.78); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 18px; padding: 16px 18px; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25); } .items-table.monedas-table td { display: flex; flex-direction: column; gap: 6px; padding: 0; border: none; font-size: 0.95rem; } .items-table.monedas-table td::before { content: attr(data-label); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--secondary); opacity: 0.9; } .items-table.monedas-table td[data-label="Acciones"] { gap: 10px; } .items-table.monedas-table td[data-label="Acciones"] .action-btn { width: 100%; justify-content: center; } .moneda-actions { width: 100%; flex-direction: column; } .users-table { display: table; font-size: 0.93rem; } .users-table th, .users-table td { padding: 8px 6px; font-size: 0.93rem; word-break: break-word; } .users-table th { padding-top: 12px; padding-bottom: 12px; } .form-container { padding: 10px 2px; } } @media (max-width: 500px) { .users-table th, .users-table td { padding: 6px 2px; font-size: 0.89rem; } .form-container { padding: 4px 0; } } /* Secciones ocultas */ .section { display: none; animation: fadeIn 0.5s; } .section.active { display: block; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } /* Modal personalizado */ .custom-modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100vw; height: 100vh; background: rgba(44, 62, 80, 0.85); align-items: center; justify-content: center; transition: opacity 0.3s; } .custom-modal.active { display: flex; animation: modalFadeIn 0.4s; } @keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } } .modal-content { background: rgba(255,255,255,0.12); border-radius: 18px; padding: 40px 30px 30px 30px; box-shadow: 0 8px 32px rgba(0,0,0,0.35); text-align: center; max-width: 95vw; width: 370px; position: relative; animation: modalPop 0.5s cubic-bezier(.68,-0.55,.27,1.55); } @keyframes modalPop { 0% { transform: scale(0.7); opacity: 0; } 80% { transform: scale(1.05); } 100% { transform: scale(1); opacity: 1; } } .modal-icon { width: 70px; height: 70px; border-radius: 50%; background: linear-gradient(135deg, #3498db 0%, #e74c3c 100%); display: flex; align-items: center; justify-content: center; margin: 0 auto 18px auto; font-size: 2.5rem; color: white; box-shadow: 0 4px 18px rgba(0,0,0,0.18); animation: iconBounce 1s; } @keyframes iconBounce { 0% { transform: scale(0.5);} 60% { transform: scale(1.2);} 100% { transform: scale(1);} } .modal-content h2 { color: white; font-size: 1.6rem; margin-bottom: 10px; font-weight: 700; } .modal-content p { color: var(--light); font-size: 1.1rem; margin-bottom: 25px; } .modal-actions { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } @media (max-width: 500px) { .modal-content { padding: 25px 10px 20px 10px; width: 95vw; } .modal-content h2 { font-size: 1.2rem; } .modal-content p { font-size: 1rem; } .modal-icon { width: 55px; } } #compras-section .content-header { flex-wrap: wrap; gap: 12px; } #compras-section .content-header .compras-filtros { display: flex; align-items: center; gap: 10px; } #compras-section .content-header label { font-weight: 600; color: var(--light); font-size: 0.92rem; } #compras-section .content-header .filtro-fecha { display: flex; align-items: center; gap: 8px; } #compras-section .content-header .filtro-fecha input { width: 140px; padding: 6px 8px; font-size: 0.9rem; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.18); color: #e5e7eb; border-radius: 6px; } #compras-section .content-header .filtro-fecha input:focus { border-color: var(--secondary); outline: none; background: rgba(255, 255, 255, 0.12); } #compras-section .form-control[type="date"]::-webkit-calendar-picker-indicator { filter: invert(0.8); } #compras-section .btn-success { background: linear-gradient(135deg, var(--success) 0%, #34d399 100%); color: white; } #compras-section .btn-primary { background: linear-gradient(135deg, var(--secondary) 0%, var(--accent) 100%); color: white; } .venta-modal { width: min(92vw, 520px); background: linear-gradient(135deg, #0b1220 0%, #0f172a 100%); border: 1px solid rgba(255, 255, 255, 0.12); box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6); padding: 18px 16px 16px 16px; max-height: 88vh; overflow: auto; } .venta-modal .modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .venta-modal .modal-header h2 { margin: 0; color: var(--secondary); font-size: 1.25rem; display: flex; align-items: center; gap: 10px; } .venta-modal .modal-header button { background: none; border: none; color: #ef4444; font-size: 1.1rem; cursor: pointer; padding: 4px; border-radius: 50%; transition: transform 0.2s ease, background 0.3s ease; } .venta-modal .modal-header button:hover { transform: scale(1.05); background: rgba(239, 68, 68, 0.12); } .venta-modal .form-section { margin-bottom: 16px; } .venta-modal .form-section:last-of-type { margin-bottom: 0; } .venta-modal .form-section h3 { color: var(--secondary); font-size: 1rem; margin-bottom: 12px; border-left: 2px solid rgba(255, 255, 255, 0.12); padding-left: 8px; display: flex; align-items: center; gap: 6px; } .venta-modal .form-section h3 i { color: var(--accent); } .venta-modal .form-row { display: flex; gap: 12px; flex-wrap: wrap; } .venta-modal .form-row .form-col { flex: 1; min-width: 180px; } .venta-modal .form-actions { margin-top: 16px; display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; } .venta-modal .form-actions .btn { padding: 10px 16px; font-size: 0.95rem; font-weight: 700; border-radius: 10px; } /* ajustes locales de tipografía y campos para compactar */ .venta-modal .form-control { font-size: 0.95rem; padding: 8px 10px; background: #f8fafc; color: #0f172a; border: 1px solid rgba(15, 23, 42, 0.12); } .venta-modal .form-group label { font-size: 0.9rem; } .venta-modal .form-control::placeholder { color: rgba(15, 23, 42, 0.55); } .venta-modal select.form-control { appearance: none; background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8"%3E%3Cpath fill="%230f172a" d="M6 8L0 0h12z"/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 8px; padding-right: 32px; } .venta-modal select.form-control option { color: #0f172a; background: #f8fafc; } @media (max-width: 600px) { .venta-modal { padding: 16px 12px; width: 94vw; } .venta-modal .modal-header h2 { font-size: 1.1rem; } .venta-modal .form-row .form-col { min-width: 100%; } .venta-modal .form-actions { justify-content: center; } .venta-modal .form-actions .btn { padding: 10px 12px; font-size: 0.9rem; } #compras-section .content-header { gap: 10px; } #compras-section .content-header .compras-filtros { flex-direction: column; align-items: stretch; width: 100%; gap: 12px; } #compras-section .content-header .filtro-fecha { justify-content: space-between; } #compras-section .content-header .filtro-fecha input { width: 100%; } } #compras-cards-container { display: flex; flex-direction: column; gap: 28px; padding: 18px 0; background: transparent; } .compras-empty { color: rgba(255,255,255,0.65); text-align: center; padding: 18px 0; } .compra-card { margin: 0 !important; border-radius: 18px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.04); padding: 24px; } .compra-card-body { display: flex; align-items: stretch; gap: 28px; flex-wrap: wrap; } .compra-card-left { flex: 1 1 320px; display: flex; flex-direction: column; gap: 18px; } .compra-card-right { flex: 0 0 240px; display: flex; flex-direction: column; align-items: flex-end; gap: 18px; } .compra-card-header { display: flex; align-items: center; gap: 16px; } .compra-avatar { width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,0.08); display: flex; align-items: center; justify-content: center; overflow: hidden; font-size: 1.2rem; color: rgba(255,255,255,0.75); } .compra-avatar img { width: 48px; height: 48px; object-fit: cover; } .compra-card-title { font-weight: 700; color: #fff; font-size: 1.1rem; } .compra-card-title .compra-label { color: rgba(255,255,255,0.6); font-weight: 600; margin-right: 6px; } .compra-card-meta { display: flex; gap: 16px; flex-wrap: wrap; color: rgba(255,255,255,0.75); } .compra-card-meta i { color: rgba(255,255,255,0.55); } .compra-card-proyecto { display: flex; align-items: center; gap: 16px; } .compra-proyecto-icon { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); font-size: 1.4rem; } .compra-proyecto-detalle { display: flex; flex-direction: column; gap: 4px; } .compra-proyecto-tipo { font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255,255,255,0.55); } .compra-item-link, .compra-item-text { font-size: 1.02rem; font-weight: 700; } .compra-item-link { color: var(--secondary); text-decoration: none; } .compra-item-link:hover { text-decoration: underline; } .compra-item-text { color: var(--light); } .compra-card-precio { text-align: right; } .compra-precio-label { font-size: 0.8rem; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255,255,255,0.55); display: block; } .compra-precio-monto { font-size: 1.9rem; font-weight: 800; color: var(--success); } .compra-card-fecha { display: inline-flex; align-items: center; gap: 7px; background: rgba(255,255,255,0.06); color: var(--light); border: 1px solid rgba(255,255,255,0.12); padding: 6px 12px; border-radius: 10px; } .compra-whatsapp { color: var(--success); margin-left: 6px; } .compra-whatsapp:hover { opacity: 0.9; } .compra-card-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; } .tipo-icon.tipo-proyecto { color: var(--accent); } .tipo-icon.tipo-servicio { color: var(--secondary); } .btn-cancel { background: rgba(255,255,255,0.1); color: #fff; } .btn-cancel:hover { background: rgba(255,255,255,0.16); } @media (max-width: 900px) { .compra-card-body { flex-direction: column; gap: 20px; } .compra-card-right { flex: 1 1 auto; align-items: flex-start; gap: 16px; } .compra-card-precio { text-align: left; } .compra-card-actions { justify-content: flex-start; } } .btn-cancel { background: rgba(255,255,255,0.1); color: #fff; } .btn-cancel:hover { background: rgba(255,255,255,0.16); } @media (max-width: 900px) { .compra-card-body { flex-direction: column; gap: 20px; } .compra-card-right { width: 100%; align-items: flex-start; gap: 16px; } .compra-card-precio { text-align: left; } .compra-card-actions { justify-content: flex-start; } } @media (max-width: 600px) { .compra-card { padding: 18px; border-radius: 16px; } .compra-card-body { gap: 16px; } .compra-card-header { flex-direction: column; align-items: flex-start; gap: 10px; } .compra-card-meta { flex-direction: column; align-items: flex-start; gap: 8px; } .compra-card-right { gap: 12px; } .compra-card-precio { width: 100%; } .compra-precio-monto { font-size: 1.5rem; } .compra-card-fecha { width: 100%; justify-content: flex-start; } .compra-card-actions { width: 100%; gap: 10px; justify-content: stretch; } .compra-card-actions .btn { flex: 1 1 48%; min-width: 130px; justify-content: center; } } </style> <script> // --- CLIENTES SATISFECHOS: DISEÑO ESPECTACULAR --- // Función para previsualizar logos function previsualizarLogo(input, previewId) { const preview = document.getElementById(previewId); if (input.files && input.files[0]) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = `<img src="${e.target.result}" alt="Preview" style="max-width: 100px; max-height: 100px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">`; }; reader.readAsDataURL(input.files[0]); } } // Función para cargar clientes con diseño espectacular function cargarClientesSatisfechos() { fetch('clientes.php?action=list') .then(r => r.json()) .then(items => { const tbody = document.getElementById('clientes-table-body'); const cardsContainer = document.getElementById('clientes-cards-container'); if (!tbody && !cardsContainer) return; if (tbody) tbody.innerHTML = ''; if (cardsContainer) cardsContainer.innerHTML = ''; if (!Array.isArray(items) || !items.length) { if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#aaa;">No hay clientes registrados.</td></tr>'; if (cardsContainer) cardsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:18px 0;">No hay clientes registrados.</div>'; return; } items.forEach(cliente => { const telefono = cliente.telefono ?? ''; const documento = cliente.documento ?? ''; const direccion = cliente.direccion ?? ''; const foto = cliente.foto ?? ''; const fotoHtml = foto ? `<img src="${foto}" alt="Foto" style="width:42px;height:42px;object-fit:cover;border-radius:10px;border:1px solid rgba(255,255,255,0.16);">` : `<div style="width:42px;height:42px;border-radius:10px;border:1px solid rgba(255,255,255,0.16);background:rgba(255,255,255,0.06);"></div>`; if (tbody) { tbody.innerHTML += ` <tr> <td>${fotoHtml}</td> <td>${cliente.nombre}</td> <td>${telefono}</td> <td>${documento}</td> <td>${direccion}</td> <td> <div class="acciones-inline"> <button class="action-btn edit-btn" onclick="abrirModalEditarCliente(${cliente.id})" title="Editar"> <i class="fas fa-edit"></i> </button> <button class="action-btn delete-btn" onclick="eliminarClienteEspectacular(${cliente.id})" title="Eliminar"> <i class="fas fa-trash"></i> </button> </div> </td> </tr> `; } if (cardsContainer) { const card = document.createElement('div'); card.className = 'service-card'; card.innerHTML = ` <div class="service-card-title" style="display:flex;align-items:center;gap:10px;"> ${foto ? `<img src="${foto}" alt="Foto" style="width:42px;height:42px;object-fit:cover;border-radius:12px;border:1px solid rgba(255,255,255,0.16);">` : ''} <span>${cliente.nombre}</span> </div> <div class="service-card-meta"> <span><b>Teléfono:</b> ${telefono}</span> <span><b>Documento:</b> ${documento}</span> </div> <div class="service-card-desc"><b>Dirección:</b> ${direccion}</div> <div class="service-card-actions"> <button class="action-btn edit-btn" onclick="abrirModalEditarCliente(${cliente.id})" title="Editar"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarClienteEspectacular(${cliente.id})" title="Eliminar"><i class="fas fa-trash"></i></button> </div> `; cardsContainer.appendChild(card); } }); }) .catch(e => { console.error('Error cargando clientes:', e); const tbody = document.getElementById('clientes-table-body'); if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#aaa;">Error al cargar clientes.</td></tr>'; }); } // Función para abrir modal de agregar cliente function abrirModalAgregarCliente() { document.getElementById('add-cliente-modal').classList.add('active'); document.body.style.overflow = 'hidden'; } // Función para cerrar modal de agregar cliente function cerrarModalAgregarCliente() { document.getElementById('add-cliente-modal').classList.remove('active'); document.body.style.overflow = ''; document.getElementById('add-cliente-form').reset(); const p = document.getElementById('cliente-foto-preview'); if (p) p.innerHTML = ''; } // Función para abrir modal de editar cliente function abrirModalEditarCliente(id) { fetch('clientes.php?action=get&id=' + id) .then(r => r.json()) .then(cliente => { if (!cliente || !cliente.id) { mostrarNotificacionEspectacular('Cliente no encontrado', 'error'); return; } document.getElementById('edit-cliente-id').value = cliente.id; document.getElementById('edit-cliente-nombre').value = cliente.nombre; document.getElementById('edit-cliente-telefono').value = cliente.telefono || ''; document.getElementById('edit-cliente-documento').value = cliente.documento || ''; document.getElementById('edit-cliente-direccion').value = cliente.direccion || ''; const preview = document.getElementById('edit-cliente-foto-preview'); if (preview) { preview.innerHTML = (cliente.foto ? `<img src="${cliente.foto}" alt="Foto" style="max-width: 100px; max-height: 100px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">` : ''); } document.getElementById('edit-cliente-modal').classList.add('active'); document.body.style.overflow = 'hidden'; }) .catch(e => { console.error('Error cargando cliente:', e); mostrarNotificacionEspectacular('Error al cargar datos del cliente', 'error'); }); } // Función para cerrar modal de editar cliente function cerrarModalEditarCliente() { document.getElementById('edit-cliente-modal').classList.remove('active'); document.body.style.overflow = ''; document.getElementById('edit-cliente-form').reset(); const p = document.getElementById('edit-cliente-foto-preview'); if (p) p.innerHTML = ''; } document.addEventListener('DOMContentLoaded', () => { const cancelAddClienteHeaderBtn = document.getElementById('cancel-add-cliente'); const cancelAddClienteFooterBtn = document.getElementById('cancel-add-cliente-btn'); const cancelEditClienteHeaderBtn = document.getElementById('cancel-edit-cliente'); const cancelEditClienteFooterBtn = document.getElementById('cancel-edit-cliente-btn'); cancelAddClienteHeaderBtn?.addEventListener('click', cerrarModalAgregarCliente); cancelAddClienteFooterBtn?.addEventListener('click', cerrarModalAgregarCliente); cancelEditClienteHeaderBtn?.addEventListener('click', cerrarModalEditarCliente); cancelEditClienteFooterBtn?.addEventListener('click', cerrarModalEditarCliente); }); // Función para eliminar cliente con confirmación espectacular function eliminarClienteEspectacular(id) { if (confirm('¿Estás seguro de que deseas eliminar este cliente?')) { fetch('clientes.php?action=delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }) .then(r => r.json()) .then(res => { if (res.success) { mostrarNotificacionEspectacular('Cliente eliminado exitosamente', 'success'); cargarClientesSatisfechos(); actualizarClientesSatisfechosTotales(); } else { mostrarNotificacionEspectacular(res.error || 'No se pudo eliminar el cliente', 'error'); } }) .catch(e => { console.error('Error deleting client:', e); mostrarNotificacionEspectacular('Error de conexión al eliminar cliente', 'error'); }); } } // Función para mostrar notificaciones espectaculares function mostrarNotificacionEspectacular(mensaje, tipo) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 12px; color: white; font-weight: 600; z-index: 20000; box-shadow: 0 8px 25px rgba(0,0,0,0.2); transform: translateX(400px); transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); max-width: 350px; `; if (tipo === 'success') { notification.style.background = 'linear-gradient(135deg, #27ae60 0%, #2ecc71 100%)'; notification.innerHTML = `<i class="fas fa-check-circle" style="margin-right: 10px;"></i>${mensaje}`; } else { notification.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)'; notification.innerHTML = `<i class="fas fa-exclamation-circle" style="margin-right: 10px;"></i>${mensaje}`; } document.body.appendChild(notification); setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); setTimeout(() => { notification.style.transform = 'translateX(400px)'; setTimeout(() => { document.body.removeChild(notification); }, 400); }, 3000); } // Event listeners para los formularios document.addEventListener('DOMContentLoaded', function() { // Botón para abrir modal de agregar cliente const addBtn = document.getElementById('add-cliente-btn'); if (addBtn) { addBtn.addEventListener('click', abrirModalAgregarCliente); } // Formulario de agregar cliente const addForm = document.getElementById('add-cliente-form'); if (addForm) { addForm.addEventListener('submit', function(e) { e.preventDefault(); const nombre = (document.getElementById('cliente-nombre').value || '').trim(); const telefono = (document.getElementById('cliente-telefono').value || '').trim(); const documento = (document.getElementById('cliente-documento').value || '').trim(); const direccion = (document.getElementById('cliente-direccion').value || '').trim(); const fotoFile = document.getElementById('cliente-foto')?.files?.[0] || null; const fd = new FormData(); fd.append('nombre', nombre); fd.append('telefono', telefono); fd.append('documento', documento); fd.append('direccion', direccion); if (fotoFile) fd.append('foto', fotoFile); fetch('clientes.php?action=add', { method: 'POST', body: fd }) .then(r => r.json()) .then(res => { if (res.success) { mostrarNotificacionEspectacular('Cliente agregado exitosamente', 'success'); cerrarModalAgregarCliente(); cargarClientesSatisfechos(); actualizarClientesSatisfechosTotales(); } else { mostrarNotificacionEspectacular(res.error || 'No se pudo guardar el cliente', 'error'); } }) .catch(e => { console.error('Error agregando cliente:', e); mostrarNotificacionEspectacular('Error de conexión al agregar cliente', 'error'); }); }); } // Formulario de editar cliente const editForm = document.getElementById('edit-cliente-form'); if (editForm) { editForm.addEventListener('submit', function(e) { e.preventDefault(); const id = (document.getElementById('edit-cliente-id').value || '').trim(); const nombre = (document.getElementById('edit-cliente-nombre').value || '').trim(); const telefono = (document.getElementById('edit-cliente-telefono').value || '').trim(); const documento = (document.getElementById('edit-cliente-documento').value || '').trim(); const direccion = (document.getElementById('edit-cliente-direccion').value || '').trim(); const fotoFile = document.getElementById('edit-cliente-foto')?.files?.[0] || null; const fd = new FormData(); fd.append('id', id); fd.append('nombre', nombre); fd.append('telefono', telefono); fd.append('documento', documento); fd.append('direccion', direccion); if (fotoFile) fd.append('foto', fotoFile); fetch('clientes.php?action=edit', { method: 'POST', body: fd }) .then(r => r.json()) .then(res => { if (res.success) { mostrarNotificacionEspectacular('Cliente actualizado exitosamente', 'success'); cerrarModalEditarCliente(); cargarClientesSatisfechos(); actualizarClientesSatisfechosTotales(); } else { mostrarNotificacionEspectacular(res.error || 'No se pudo actualizar el cliente', 'error'); } }) .catch(e => { console.error('Error actualizando cliente:', e); mostrarNotificacionEspectacular('Error de conexión al actualizar cliente', 'error'); }); }); } // Cerrar modales al hacer clic fuera document.getElementById('add-cliente-modal').addEventListener('click', function(e) { if (e.target === this) cerrarModalAgregarCliente(); }); document.getElementById('edit-cliente-modal').addEventListener('click', function(e) { if (e.target === this) cerrarModalEditarCliente(); }); }); function actualizarClientesSatisfechosTotales() { fetch('clientes.php?action=count') .then(r => r.json()) .then(d => { const el = document.getElementById('clientes-satisfechos-num'); if (el) el.textContent = (d.total ?? 0); }); } // Carga inicial de clientes y actualización de contador document.addEventListener('DOMContentLoaded', function() { try { cargarClientesSatisfechos(); } catch (e) {} try { actualizarClientesSatisfechosTotales(); } catch (e) {} }); </script> <style> /* Estilos responsivos para el modal de editar compra */ .edit-service-modal-content { max-width: 90vw; width: 500px; max-height: 90vh; overflow-y: auto; background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); border: 2px solid #3498db; box-shadow: 0 20px 60px rgba(0,0,0,0.5); } @media (max-width: 768px) { .edit-service-modal-content { width: 95vw; margin: 10px; padding: 20px; } .form-row { flex-direction: column; } .form-col { width: 100%; margin-bottom: 15px; } .form-actions { flex-direction: column; gap: 10px; } .form-actions .btn { width: 100%; margin: 0; } } @media (max-width: 480px) { .edit-service-modal-content { padding: 15px; } .edit-service-modal-content h2 { font-size: 1.5rem; margin-bottom: 15px; } .form-group label { font-size: 0.9rem; } .form-control { padding: 8px 10px; font-size: 0.9rem; } .modal-header { flex-direction: column; gap: 10px; text-align: center; } .modal-header h2 { font-size: 1.4rem; } .form-section h3 { font-size: 1rem; } .form-actions { flex-direction: column; } .form-actions .btn { width: 100%; margin: 5px 0; } } /* Estilos específicos para el botón Nueva Venta */ #btn-nueva-venta:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(39,174,96,0.4); } /* Animación para el icono del contador */ .admin-logo div:first-child { transition: all 0.3s ease; } .admin-logo div:first-child:hover { transform: scale(1.05) rotate(5deg); box-shadow: 0 12px 35px rgba(52,152,219,0.6); } .admin-logo div:first-child i { transition: all 0.3s ease; } .admin-logo div:first-child:hover i { transform: scale(1.1); text-shadow: 0 4px 8px rgba(0,0,0,0.4); } #btn-nueva-venta:active { transform: translateY(0); } /* Estilos para las secciones del formulario */ .form-section { background: rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; border: 1px solid rgba(255,255,255,0.15); backdrop-filter: blur(10px); } .form-section h3 { margin-top: 0; font-weight: 700; letter-spacing: 0.5px; } /* Animaciones para el modal */ @keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } .edit-service-modal-content { animation: slideInUp 0.4s ease-out; } /* Estilos específicos para mejorar legibilidad en modales */ .edit-service-modal-content .form-control { background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.3); color: #fff; font-weight: 500; } .edit-service-modal-content .form-control:focus { background: rgba(255,255,255,0.25); border-color: #3498db; box-shadow: 0 0 20px rgba(52,152,219,0.4); } .edit-service-modal-content .form-control::placeholder { color: rgba(255,255,255,0.7); } .edit-service-modal-content label { color: #fff; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.3); } .edit-service-modal-content h2, .edit-service-modal-content h3 { text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .edit-service-modal-content select.form-control option { background: #2c3e50; color: #fff; } /* Mejorar contraste de los botones */ .edit-service-modal-content .btn-primary { background: linear-gradient(90deg,#3498db 0%,#2980b9 100%); border: none; font-weight: 700; text-shadow: 0 1px 2px rgba(0,0,0,0.3); box-shadow: 0 4px 15px rgba(52,152,219,0.4); } .edit-service-modal-content .btn-primary:hover { background: linear-gradient(90deg,#2980b9 0%,#1f5f8b 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(52,152,219,0.5); } /* MODAL DE EDICIÓN DE SERVICIO PERSONALIZADO */ .edit-service-modal-content { background: #232946; border-radius: 16px; padding: 10px 10px 6px 10px; box-shadow: 0 8px 32px rgba(44,62,80,0.35), 0 2px 12px #27ae6033; text-align: center; width: 96vw; max-width: 320px; height: auto; max-height: 95vh; border: 3px solid #3498db; position: relative; animation: modalPop 0.5s cubic-bezier(.68,-0.55,.27,1.55); display: flex; flex-direction: column; justify-content: center; align-items: center; overflow-y: auto; } .edit-service-modal-content h2 { color: #fff; font-size: 1rem; margin-bottom: 6px; font-weight: 700; } .edit-service-modal-content label { color: #27ae60; font-weight: 600; font-size: 0.92rem; margin-bottom: 2px; } .edit-service-modal-content .form-control { background: #fff; color: #232946; border: 1.2px solid #3498db; border-radius: 7px; font-size: 0.92rem; margin-bottom: 4px; padding: 6px 8px; width: 100%; min-width: 0; box-sizing: border-box; } .edit-service-modal-content .form-row { display: flex; gap: 6px; } .edit-service-modal-content .form-col { flex: 1; } .edit-service-modal-content .form-actions { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-top: 4px; } @media (max-width: 400px) { .edit-service-modal-content { width: 98vw; height: auto; min-height: 0; padding: 6px 2vw 4px 2vw; } } .services-cards-container { display: none; flex-direction: column; gap: 18px; margin-top: 18px; } .service-card { background: rgba(255,255,255,0.08); border-radius: 16px; box-shadow: 0 4px 18px rgba(44,62,80,0.13); padding: 22px 18px 16px 18px; display: flex; flex-direction: column; gap: 10px; position: relative; border: 1.5px solid rgba(52,152,219,0.13); } .service-card-title { font-size: 1.18rem; font-weight: 700; color: var(--secondary); margin-bottom: 2px; } .service-card-desc { color: #fff; font-size: 1rem; margin-bottom: 4px; } .service-card-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 0.98rem; color: #b2e0ff; margin-bottom: 8px; } .service-card-actions { display: flex; gap: 10px; margin-top: 6px; } @media (max-width: 700px) { .items-table { display: none; } .services-cards-container { display: flex; } } @media (max-width: 500px) { .service-card { padding: 14px 7px 10px 7px; } .service-card-title { font-size: 1.05rem; } .service-card-desc { font-size: 0.97rem; } } .projects-cards-container { display: none; flex-direction: column; gap: 18px; margin-top: 18px; } .project-card { background: rgba(255,255,255,0.08); border-radius: 16px; box-shadow: 0 4px 18px rgba(44,62,80,0.13); padding: 22px 18px 16px 18px; display: flex; flex-direction: column; gap: 10px; position: relative; border: 1.5px solid rgba(52,152,219,0.13); } .project-card-header { display: flex; align-items: center; gap: 14px; margin-bottom: 6px; } .project-card-img { width: 60px; height: 40px; border-radius: 8px; object-fit: cover; box-shadow: 0 2px 8px #27ae6033; background: #232946; display: flex; align-items: center; justify-content: center; } .project-card-title { font-size: 1.08rem; font-weight: 700; color: var(--secondary); } .project-card-desc { color: #fff; font-size: 0.97rem; margin-bottom: 2px; } .project-card-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 0.95rem; color: #b2e0ff; margin-bottom: 8px; } .project-card-price { color: #27ae60; font-weight: 700; font-size: 1.01rem; } .project-card-actions { display: flex; gap: 10px; margin-top: 6px; } @media (max-width: 700px) { .items-table { display: none; } .projects-cards-container { display: flex; } } @media (max-width: 500px) { .project-card { padding: 14px 7px 10px 7px; } .project-card-title { font-size: 1.01rem; } .project-card-desc { font-size: 0.95rem; } } </style> <style> .user-card-responsive { background: linear-gradient(135deg, #232946 60%, #3498db 100%); border-radius: 18px; box-shadow: 0 4px 18px rgba(44,62,80,0.18), 0 1.5px 8px rgba(39,174,96,0.10); padding: 18px 14px 12px 14px; margin-bottom: 18px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 2.5px solid #27ae60; position: relative; overflow: hidden; animation: fadeIn 0.5s; } .user-card-responsive:hover { transform: translateY(-6px) scale(1.03); box-shadow: 0 12px 32px rgba(39,174,96,0.18), 0 2px 12px rgba(44,62,80,0.13); border: 2.5px solid #3498db; } .user-card-avatar { width: 54px; height: 54px; border-radius: 50%; background: linear-gradient(135deg, #27ae60 0%, #3498db 100%); color: #fff; font-weight: 900; font-size: 1.7rem; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; box-shadow: 0 2px 8px #27ae6033; border: 2.5px solid #fff; position: absolute; top: 14px; right: 14px; } .user-card-title { font-size: 1.13rem; font-weight: 800; color: #f1c40f; margin-bottom: 2px; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; } .user-card-meta { color: #fff; font-size: 0.98rem; margin-bottom: 2px; display: flex; align-items: center; gap: 7px; } .user-card-label { color: #b2e0ff; font-weight: 700; font-size: 0.97rem; display: flex; align-items: center; gap: 4px; } .user-card-value { color: #fff; font-weight: 600; font-size: 0.97rem; } .user-card-actions { display: flex; gap: 10px; margin-top: 6px; } @media (max-width: 500px) { .user-card-responsive { padding: 10px 4px 8px 8px; border-radius: 14px; } .user-card-avatar { width: 40px; height: 40px; font-size: 1.1rem; top: 8px; right: 8px; } .user-card-title { font-size: 1.01rem; } .user-card-meta { font-size: 0.93rem; } } /* Estilos anteriores para tarjetas de compras reemplazados por la nueva paleta */ .desktop-only { display: block; } .mobile-only { display: none; } @media (max-width: 700px) { .desktop-only { display: none !important; } .mobile-only { display: block !important; } } .current-date { display: flex; align-items: center; gap: 10px; font-size: 1.15rem; font-weight: 700; color: #27ae60; background: linear-gradient(90deg, #232946 60%, #27ae60 100%); padding: 10px 22px; border-radius: 16px; box-shadow: 0 2px 12px #27ae6033; margin-left: 18px; transition: background 0.3s, color 0.3s; animation: dateFadeIn 1.2s cubic-bezier(.68,-0.55,.27,1.55); } .current-date i { color: #f1c40f; font-size: 1.3em; animation: dateIconPop 1.2s cubic-bezier(.68,-0.55,.27,1.55); } @keyframes dateFadeIn { 0% { opacity: 0; transform: translateY(-20px) scale(0.8); } 80% { opacity: 1; transform: scale(1.05); } 100% { opacity: 1; transform: translateY(0) scale(1); } } @keyframes dateIconPop { 0% { transform: scale(0.5); } 60% { transform: scale(1.3); } 100% { transform: scale(1); } } @media (max-width: 700px) { .current-date { font-size: 1.08rem; padding: 10px 10px; margin-left: 0; margin-top: 10px; justify-content: center; width: 100%; background: linear-gradient(90deg, #f1c40f 0%, #27ae60 100%); color: #232946; box-shadow: 0 2px 12px #f1c40f33; border: 2.5px solid #27ae60; } .current-date i { color: #232946; text-shadow: 0 2px 8px #f1c40f33; } } </style> <style> #compras-cards-container { display: flex; flex-direction: column; gap: 28px; padding: 18px 0; background: transparent; } .compra-card { margin: 0 !important; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.04); box-shadow: none; transition: none; } .compra-card:hover { transform: none; box-shadow: none; border-color: inherit; } @media (max-width: 700px) { #compras-cards-container { gap: 18px; padding: 10px 0; } .compra-card { border-radius: 14px; } } /* ===== CLIENTES SATISFECHOS - DISEÑO ESPECTACULAR ===== */ .clientes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 25px; margin: 20px 0; } .cliente-card { background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 20px; padding: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.1), 0 1px 8px rgba(0,0,0,0.05); border: 1px solid rgba(255,255,255,0.2); transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); position: relative; overflow: hidden; backdrop-filter: blur(10px); } .cliente-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, #3498db, #2ecc71, #f1c40f, #e74c3c); border-radius: 20px 20px 0 0; } .cliente-card:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 20px 40px rgba(0,0,0,0.15), 0 10px 20px rgba(52,152,219,0.1); border-color: rgba(52,152,219,0.3); } .cliente-logo-container { display: flex; justify-content: center; margin-bottom: 20px; position: relative; } .cliente-logo { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; border: 4px solid #fff; box-shadow: 0 8px 25px rgba(0,0,0,0.1); transition: all 0.3s ease; } .cliente-logo:hover { transform: scale(1.1); box-shadow: 0 12px 35px rgba(52,152,219,0.3); } .cliente-logo-placeholder { width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: bold; box-shadow: 0 8px 25px rgba(0,0,0,0.1); } .cliente-name { text-align: center; font-size: 1.4rem; font-weight: 700; color: #2c3e50; margin-bottom: 15px; text-shadow: 0 1px 2px rgba(0,0,0,0.1); } .cliente-video-badge { display: inline-flex; align-items: center; gap: 8px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; padding: 8px 15px; border-radius: 25px; font-size: 0.9rem; font-weight: 600; margin-bottom: 20px; box-shadow: 0 4px 15px rgba(231,76,60,0.3); transition: all 0.3s ease; } .cliente-video-badge:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(231,76,60,0.4); } .cliente-actions { display: flex; gap: 10px; justify-content: center; margin-top: 20px; } .cliente-btn { padding: 10px 20px; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.9rem; } .cliente-btn-edit { background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: white; box-shadow: 0 4px 15px rgba(52,152,219,0.3); } .cliente-btn-edit:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(52,152,219,0.4); } .cliente-btn-delete { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; box-shadow: 0 4px 15px rgba(231,76,60,0.3); } .cliente-btn-delete:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(231,76,60,0.4); } .cliente-btn-video { background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); color: white; box-shadow: 0 4px 15px rgba(155,89,182,0.3); } .cliente-btn-video:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(155,89,182,0.4); } /* Modal de edición espectacular */ .edit-cliente-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); backdrop-filter: blur(10px); z-index: 10000; align-items: center; justify-content: center; animation: modalFadeIn 0.3s ease; } .edit-cliente-modal.active { display: flex; } .edit-cliente-modal-content { background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 25px; padding: 40px; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; box-shadow: 0 25px 80px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); position: relative; animation: modalSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes modalSlideIn { 0% { opacity: 0; transform: scale(0.7) translateY(-50px); } 100% { opacity: 1; transform: scale(1) translateY(0); } } .edit-cliente-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #ecf0f1; } .edit-cliente-title { font-size: 2rem; font-weight: 700; color: #2c3e50; margin: 0; display: flex; align-items: center; justify-content: center; gap: 15px; } .edit-cliente-icon { width: 60px; height: 60px; background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 1.5rem; box-shadow: 0 8px 25px rgba(52,152,219,0.3); } .edit-cliente-form-group { margin-bottom: 25px; } .edit-cliente-label { display: block; font-weight: 700; color: #2c3e50; margin-bottom: 8px; font-size: 1.1rem; } .edit-cliente-input { width: 100%; padding: 15px 20px; border: 2px solid #ecf0f1; border-radius: 12px; font-size: 1rem; transition: all 0.3s ease; background: white; color: #2c3e50; } .edit-cliente-input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52,152,219,0.1); transform: translateY(-2px); } .edit-cliente-file-input { position: relative; overflow: hidden; display: inline-block; width: 100%; } .edit-cliente-file-input input[type=file] { position: absolute; left: -9999px; } .edit-cliente-file-label { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 15px 20px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 2px dashed #3498db; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; color: #3498db; font-weight: 600; } .edit-cliente-file-label:hover { background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: white; transform: translateY(-2px); } .edit-cliente-preview { margin-top: 15px; text-align: center; } .edit-cliente-preview img { max-width: 100px; max-height: 100px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .edit-cliente-actions { display: flex; gap: 15px; justify-content: center; margin-top: 30px; padding-top: 20px; border-top: 2px solid #ecf0f1; } .edit-cliente-btn { padding: 15px 30px; border: none; border-radius: 12px; font-weight: 700; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; gap: 10px; min-width: 140px; justify-content: center; } .edit-cliente-btn-save { background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); color: white; box-shadow: 0 4px 15px rgba(39,174,96,0.3); } .edit-cliente-btn-save:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(39,174,96,0.4); } .edit-cliente-btn-cancel { background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); color: white; box-shadow: 0 4px 15px rgba(149,165,166,0.3); } .edit-cliente-btn-cancel:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(149,165,166,0.4); } .edit-cliente-close { position: absolute; top: 20px; right: 20px; background: none; border: none; font-size: 1.5rem; color: #95a5a6; cursor: pointer; transition: all 0.3s ease; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .edit-cliente-close:hover { background: #e74c3c; color: white; transform: rotate(90deg); } /* Responsive para móviles */ @media (max-width: 768px) { .clientes-grid { grid-template-columns: 1fr; gap: 20px; } .cliente-card { padding: 20px; } .edit-cliente-modal-content { padding: 25px; margin: 20px; border-radius: 20px; } .edit-cliente-actions { flex-direction: column; } .edit-cliente-btn { width: 100%; } } /* Animaciones adicionales */ @keyframes clienteCardAppear { 0% { opacity: 0; transform: translateY(30px) scale(0.9); } 100% { opacity: 1; transform: translateY(0) scale(1); } } .cliente-card { animation: clienteCardAppear 0.6s ease forwards; } .cliente-card:nth-child(1) { animation-delay: 0.1s; } .cliente-card:nth-child(2) { animation-delay: 0.2s; } .cliente-card:nth-child(3) { animation-delay: 0.3s; } .cliente-card:nth-child(4) { animation-delay: 0.4s; } .cliente-card:nth-child(5) { animation-delay: 0.5s; } .cliente-card:nth-child(6) { animation-delay: 0.6s; } </style> </head> <body> <!-- Fondo con partículas --> <div class="particles" id="particles"></div> <!-- Overlay para móviles --> <div class="sidebar-overlay" id="sidebar-overlay"></div> <!-- Panel Administrativo --> <div class="admin-container"> <!-- Sidebar --> <div class="admin-sidebar"> <div class="sidebar-header"> <div class="admin-logo"> <img src="images/logo.png" alt="Logo de Contador Francisco"> <div>Contador Francisco</div> </div> </div> <div class="admin-menu"> <div class="menu-item active" data-section="dashboard"> <i class="fas fa-tachometer-alt"></i> <span>Dashboard</span> </div> <div class="menu-item" data-section="services"> <i class="fas fa-cogs"></i> <span>Servicios</span> </div> <div class="menu-item" data-section="projects"> <i class="fas fa-project-diagram"></i> <span>Proyectos</span> </div> <div class="menu-item" data-section="clientes"> <i class="fas fa-handshake"></i> <span>Clientes</span> </div> <div class="menu-item" data-section="compras"> <i class="fas fa-shopping-cart"></i> <span>Ventas</span> </div> <div class="menu-item" data-section="users"> <i class="fas fa-users"></i> <span>Usuarios</span> </div> <div class="menu-item" data-section="ajustes"> <i class="fas fa-sliders-h"></i> <span>Ajustes</span> </div> <div class="menu-item" id="logout-btn" style="cursor:pointer;"> <i class="fas fa-sign-out-alt"></i> <span>Cerrar Sesión</span> </div> </div> </div> <!-- Main Content --> <div class="admin-main"> <!-- Header --> <div class="admin-header"> <div class="header-left"> <div class="toggle-sidebar"> <div class="hamburger"> <span></span> <span></span> <span></span> </div> </div> <div class="header-title">Panel Administrativo</div> </div> <div class="user-menu"> <div class="user-info"> <div class="user-name"><?php echo htmlspecialchars($nombre); ?></div> <div class="user-role"><?php echo htmlspecialchars($rol); ?></div> </div> <div class="user-avatar"><?php echo strtoupper(substr($nombre, 0, 1)); ?></div> </div> </div> <!-- Content --> <div class="admin-content"> <!-- Dashboard --> <div class="section active" id="dashboard-section"> <div class="content-header"> <div class="content-title">Dashboard</div> <div class="current-date" id="currentDate"></div> </div> <div class="stats-grid"> <div class="stat-card"> <div class="stat-icon"> <i class="fas fa-cogs"></i> </div> <div class="stat-info"> <h3 id="servicios-activos-num">Cargando...</h3> <p>Servicios Activos</p> </div> </div> <div class="stat-card"> <div class="stat-icon"> <i class="fas fa-project-diagram"></i> </div> <div class="stat-info"> <h3 id="proyectos-completados-num">Cargando...</h3> <p>Proyectos Completados</p> </div> </div> <div class="stat-card"> <div class="stat-icon"> <i class="fas fa-users"></i> </div> <div class="stat-info"> <h3>Cargando...</h3> <p>Usuarios</p> </div> </div> <div class="stat-card"> <div class="stat-icon"> <i class="fas fa-handshake"></i> </div> <div class="stat-info"> <h3 id="clientes-satisfechos-num">Cargando...</h3> <p>Clientes</p> </div> </div> </div> </div> <!-- Servicios --> <div class="section" id="services-section"> <div class="content-header"> <div class="content-title">Gestión de Servicios</div> <button class="btn btn-success" id="add-service"> <i class="fas fa-plus-circle"></i> Nuevo Servicio </button> </div> <!-- Formulario de agregar servicio eliminado, ahora solo se usa el modal --> <div class="form-container"> <div class="services-cards-container" id="services-cards-container"> <!-- Las tarjetas de servicios se renderizan aquí en móviles --> </div> <table class="items-table"> <thead> <tr> <th>Nombre</th> <th>Descripción</th> <th>Categoría</th> <th>Precio</th> <th>Acciones</th> </tr> </thead> <tbody id="services-table-body"> <!-- Los servicios se cargarán aquí dinámicamente --> </tbody> </table> </div> </div> <!-- Proyectos --> <div class="section" id="projects-section"> <div class="content-header"> <div class="content-title">Gestión de Proyectos</div> <button class="btn btn-success" id="add-project"> <i class="fas fa-plus-circle"></i> Nuevo Proyecto </button> </div> <div class="form-container"> <div class="projects-cards-container" id="projects-cards-container"></div> <table class="items-table"> <thead> <tr> <th>Imagen</th> <th>Nombre</th> <th>Descripción</th> <th>Características</th> <th>Precio</th> <th>Acciones</th> </tr> </thead> <tbody id="projects-table-body"> <!-- Los proyectos se cargarán aquí dinámicamente --> </tbody> </table> </div> </div> <!-- Clientes Satisfechos --> <div class="section" id="clientes-section"> <div class="content-header"> <div class="content-title"> <i class="fas fa-handshake" style="margin-right: 12px; color: #f1c40f;"></i> Clientes </div> <button class="btn btn-success" id="add-cliente-btn" style="background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); border: none; box-shadow: 0 4px 15px rgba(39,174,96,0.3);"> <i class="fas fa-user-plus"></i> Nuevo Cliente </button> </div> <div class="form-container"> <div class="services-cards-container" id="clientes-cards-container"></div> <table class="items-table"> <thead> <tr> <th>Foto</th> <th>Nombre</th> <th>Teléfono</th> <th>Documento</th> <th>Dirección</th> <th>Acciones</th> </tr> </thead> <tbody id="clientes-table-body"> <!-- Los clientes se cargarán aquí dinámicamente --> </tbody> </table> </div> </div> <!-- Usuarios --> <div class="section" id="users-section"> <div class="content-header"> <div class="content-title">Gestión de Usuarios</div> <button class="btn btn-success" id="add-user"> <i class="fas fa-user-plus"></i> Nuevo Usuario </button> </div> <div class="form-container"> <!-- <div class="users-cards-container" id="users-cards-container"></div> --> <div class="table-responsive-users desktop-only"> <table class="items-table users-table"> <thead> <tr> <th>Nombre</th> <th>Email</th> <th>Usuario</th> <th>Acciones</th> </tr> </thead> <tbody id="users-table-body"> <!-- Los usuarios se cargarán aquí dinámicamente --> </tbody> </table> </div> <div id="users-cards-responsive" class="users-cards-responsive mobile-only"></div> </div> </div> <!-- Ajustes --> <div class="section" id="ajustes-section"> <div class="content-header"> <div class="content-title">Monedas guardadas</div> </div> <div class="form-container"> <p style="margin-top:0;color:var(--light);font-size:0.95rem;"> Agrega todas las monedas que necesites y activa la que desees usar en el sistema y la página web. </p> <div class="form-group"> <label>Agregar nueva moneda</label> <div class="form-row"> <div class="form-col"> <input type="text" id="ajustes-nueva-moneda-nombre" class="form-control" placeholder="Nombre (ej: Dólar, Euro)"> </div> <div class="form-col"> <input type="text" id="ajustes-nueva-moneda-simbolo" class="form-control" placeholder="Símbolo (ej: $, €, Bs.)" maxlength="10"> </div> </div> <div class="form-actions" style="margin-top:10px;"> <button type="button" class="btn btn-success" id="ajustes-agregar-moneda"><i class="fas fa-plus-circle"></i> Agregar moneda</button> </div> </div> <div class="table-responsive" style="margin-top:16px;"> <table class="items-table monedas-table"> <thead> <tr> <th>Nombre</th> <th>Símbolo</th> <th>Estado</th> <th>Acciones</th> </tr> </thead> <tbody id="ajustes-monedas-tbody"> <!-- Monedas guardadas --> </tbody> </table> </div> </div> </div> <!-- Sección de Compras --> <div class="section" id="compras-section"> <div class="content-header"> <div class="content-title">Ventas</div> <div class="compras-filtros"> <div class="filtro-fecha"> <label for="reporte-desde">Desde</label> <input type="date" id="reporte-desde"> </div> <div class="filtro-fecha"> <label for="reporte-hasta">Hasta</label> <input type="date" id="reporte-hasta"> </div> <button id="btn-descargar-reporte" class="btn btn-success" title="Descargar Reporte PDF"> <i class="fas fa-file-pdf"></i> Descargar Reporte </button> </div> <button id="btn-nueva-venta" class="btn btn-primary"> <i class="fas fa-plus-circle"></i> Nueva Venta </button> </div> <div class="form-container"> <div id="compras-cards-container"></div> </div> </div> </div> </div> </div> <!-- Modal de Cierre de Sesión --> <div id="logout-modal" class="custom-modal"> <div class="modal-content"> <div class="modal-icon"> <i class="fas fa-sign-out-alt"></i> </div> <h2>¿Cerrar sesión?</h2> <p>¿Estás seguro que deseas salir de tu cuenta?</p> <div class="modal-actions"> <button class="btn btn-primary" id="confirm-logout">Sí, cerrar sesión</button> <button class="btn" id="cancel-logout" style="background:rgba(255,255,255,0.1);color:white;">Cancelar</button> </div> </div> </div> <!-- Modal de Despedida --> <div id="goodbye-modal" class="custom-modal"> <div class="modal-content"> <div class="modal-icon" style="background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);"> <i class="fas fa-smile-beam"></i> </div> <h2>¡Hasta pronto!</h2> <p>Tu sesión se ha cerrado correctamente.<br>¡Que tengas un excelente día!</p> </div> </div> <!-- Modal de Edición de Servicio --> <div id="edit-service-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-edit"></i> Editar Servicio</h2> <button type="button" id="cancel-edit-service" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="edit-service-form" autocomplete="off"> <div class="form-section"> <h3><i class="fas fa-info-circle"></i> Datos del Servicio</h3> <div class="form-group"> <label for="edit-service-name">Nombre del Servicio</label> <input type="text" id="edit-service-name" class="form-control" required> </div> <div class="form-group"> <label for="edit-service-description">Descripción</label> <textarea id="edit-service-description" class="form-control" required></textarea> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="edit-service-price">Precio (<span data-moneda-simbolo>$</span>)</label> <input type="number" id="edit-service-price" class="form-control" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="edit-service-category">Categoría</label> <input type="text" id="edit-service-category" class="form-control" required> </div> </div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Cambios</button> <button type="button" class="btn btn-cancel" id="cancel-edit-service-btn"><i class="fas fa-times"></i> Cancelar</button> </div> <input type="hidden" id="edit-service-id"> </form> </div> </div> <!-- Modal de Editar Compra --> <div id="edit-compra-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-edit"></i> Editar Venta</h2> <button type="button" id="cerrar-edit-compra" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="edit-compra-form" autocomplete="off" novalidate> <div class="form-section"> <h3><i class="fas fa-user"></i>Datos del Cliente</h3> <div class="form-group"> <label for="edit-compra-cliente-select">Cliente registrado (opcional)</label> <select id="edit-compra-cliente-select" class="form-control"> <option value="">Cargando...</option> </select> </div> <div id="edit-compra-cliente-fields" style="display:none;"> <div class="form-group"> <label for="edit-compra-nombre">Nombre del Cliente</label> <input type="text" id="edit-compra-nombre" class="form-control" required> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="edit-compra-telefono">Teléfono</label> <input type="tel" id="edit-compra-telefono" class="form-control" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="edit-compra-direccion">Dirección</label> <input type="text" id="edit-compra-direccion" class="form-control" required> </div> </div> </div> <div class="form-group"> <label for="edit-compra-documento">Documento (RUC o Cédula)</label> <input type="text" id="edit-compra-documento" class="form-control" readonly> </div> </div> </div> <div class="form-section"> <h3><i class="fas fa-shopping-cart"></i>Detalles de la Compra</h3> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="edit-compra-tipo">Tipo</label> <select id="edit-compra-tipo" class="form-control" required> <option value="proyecto">Proyecto</option> <option value="servicio">Servicio</option> </select> </div> </div> <div class="form-col"> <div class="form-group"> <label for="edit-compra-precio">Precio (<span data-moneda-simbolo>$</span>)</label> <input type="number" id="edit-compra-precio" class="form-control" step="0.01" required> </div> </div> </div> <div class="form-group"> <label for="edit-compra-item">Item</label> <select id="edit-compra-item" class="form-control" required> <option value="">Seleccionar item...</option> </select> </div> <div class="form-group"> <label for="edit-compra-descripcion">Descripción (opcional)</label> <textarea id="edit-compra-descripcion" class="form-control" placeholder="Escribe una descripción..."></textarea> </div> <div class="form-group"> <button type="button" id="edit-compra-agregar-item" class="btn btn-primary"> <i class="fas fa-plus"></i> Agregar a la venta </button> </div> <div class="form-group"> <div id="edit-compra-items-container"></div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Cambios</button> <button type="button" class="btn btn-cancel" id="cancel-edit-compra"><i class="fas fa-times"></i> Cancelar</button> </div> <input type="hidden" id="edit-compra-id"> <input type="hidden" id="edit-compra-item-id"> </form> </div> </div> <!-- Modal de Nueva Venta --> <div id="nueva-venta-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-plus-circle"></i> Nueva Venta</h2> <button type="button" id="cerrar-nueva-venta" title="Cerrar"> <i class="fas fa-times"></i> </button> </div> <form id="nueva-venta-form" autocomplete="off" novalidate> <div class="form-section"> <h3><i class="fas fa-user"></i>Datos del Cliente</h3> <div class="form-group"> <label for="nueva-venta-cliente-select">Cliente registrado (opcional)</label> <select id="nueva-venta-cliente-select" class="form-control"> <option value="">Cargando...</option> </select> </div> <div id="nueva-venta-cliente-fields" style="display:none;"> <div class="form-group"> <label for="nueva-venta-nombre">Nombre del Cliente *</label> <input type="text" id="nueva-venta-nombre" class="form-control" placeholder="Ej: Juan Pérez" required> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="nueva-venta-telefono">Teléfono *</label> <input type="tel" id="nueva-venta-telefono" class="form-control" placeholder="Ej: 50521545578" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="nueva-venta-direccion">Dirección *</label> <input type="text" id="nueva-venta-direccion" class="form-control" placeholder="Ej: Nicaragua" required> </div> </div> </div> <div class="form-group"> <label for="nueva-venta-documento">Documento (RUC o Cédula)</label> <input type="text" id="nueva-venta-documento" class="form-control" readonly> </div> </div> </div> <div class="form-section"> <h3><i class="fas fa-shopping-cart"></i>Detalles de la Venta</h3> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="nueva-venta-tipo">Tipo de Venta *</label> <select id="nueva-venta-tipo" class="form-control" required> <option value="">Seleccionar tipo...</option> <option value="proyecto">Proyecto</option> <option value="servicio">Servicio</option> </select> </div> </div> <div class="form-col"> <div class="form-group"> <label for="nueva-venta-precio">Precio (<span data-moneda-simbolo>$</span>) *</label> <input type="number" id="nueva-venta-precio" class="form-control" step="0.01" placeholder="0.00" required> </div> </div> </div> <div class="form-group"> <label for="nueva-venta-item">Item *</label> <select id="nueva-venta-item" class="form-control" required> <option value="">Primero selecciona el tipo...</option> </select> </div> <div class="form-group"> <label for="nueva-venta-descripcion">Descripción (opcional)</label> <textarea id="nueva-venta-descripcion" class="form-control" placeholder="Escribe una descripción..."></textarea> </div> <div class="form-group"> <button type="button" id="nueva-venta-agregar-item" class="btn btn-primary"> <i class="fas fa-plus"></i> Agregar a la venta </button> </div> <div class="form-group"> <div id="nueva-venta-items-container"></div> </div> </div> <div class="form-section"> <h3><i class="fas fa-calendar-alt"></i>Fecha de Venta</h3> <div class="form-group"> <label for="nueva-venta-fecha">Fecha *</label> <input type="date" id="nueva-venta-fecha" class="form-control" required> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"> <i class="fas fa-save"></i> Registrar Venta </button> <button type="button" class="btn btn-cancel" id="cancelar-nueva-venta"> <i class="fas fa-times"></i> Cancelar </button> </div> </form> </div> </div> <!-- Modal de Nuevo Servicio --> <div id="add-service-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-plus-circle"></i> Nuevo Servicio</h2> <button type="button" id="cancel-add-service" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="add-service-form" autocomplete="off"> <div class="form-section"> <h3><i class="fas fa-info-circle"></i> Datos del Servicio</h3> <div class="form-group"> <label for="add-service-name">Nombre del Servicio</label> <input type="text" id="add-service-name" class="form-control" required> </div> <div class="form-group"> <label for="add-service-description">Descripción</label> <textarea id="add-service-description" class="form-control" required></textarea> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="add-service-price">Precio (<span data-moneda-simbolo>$</span>)</label> <input type="number" id="add-service-price" class="form-control" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="add-service-category">Categoría</label> <input type="text" id="add-service-category" class="form-control" required> </div> </div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Servicio</button> <button type="button" class="btn btn-cancel" id="cancel-add-service-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Nuevo Usuario --> <div id="add-user-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-user-plus"></i> Nuevo Usuario</h2> <button type="button" id="cancel-add-user" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="add-user-form" autocomplete="off"> <div class="form-section"> <h3><i class="fas fa-id-badge"></i> Datos del Usuario</h3> <div class="form-group"> <label for="add-user-nombre">Nombre</label> <input type="text" id="add-user-nombre" class="form-control" required> <div id="feedback-nombre" class="form-text-feedback"></div> </div> <div class="form-group"> <label for="add-user-email">Email</label> <input type="email" id="add-user-email" class="form-control" required> <div id="feedback-email" class="form-text-feedback"></div> </div> <div class="form-group"> <label for="add-user-usuario">Usuario</label> <input type="text" id="add-user-usuario" class="form-control" required> <div id="feedback-usuario" class="form-text-feedback"></div> </div> </div> <div class="form-section"> <h3><i class="fas fa-lock"></i> Credenciales</h3> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="add-user-password">Contraseña</label> <input type="password" id="add-user-password" class="form-control" required> <div id="feedback-password" class="form-text-feedback"></div> </div> </div> <div class="form-col"> <div class="form-group"> <label for="add-user-password2">Confirmar Contraseña</label> <input type="password" id="add-user-password2" class="form-control" required> <div id="feedback-password2" class="form-text-feedback"></div> </div> </div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary" id="btn-save-user"><i class="fas fa-save"></i> Guardar Usuario</button> <button type="button" class="btn btn-cancel" id="cancel-add-user-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Edición de Usuario --> <div id="edit-user-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-user-edit"></i> Editar Usuario</h2> <button type="button" id="cancel-edit-user" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="edit-user-form" autocomplete="off"> <input type="hidden" id="edit-user-id"> <div class="form-section"> <h3><i class="fas fa-id-badge"></i> Datos del Usuario</h3> <div class="form-group"> <label for="edit-user-nombre">Nombre</label> <input type="text" id="edit-user-nombre" class="form-control" required> </div> <div class="form-group"> <label for="edit-user-email">Email</label> <input type="email" id="edit-user-email" class="form-control" required> </div> <div class="form-group"> <label for="edit-user-usuario">Usuario</label> <input type="text" id="edit-user-usuario" class="form-control" required> </div> </div> <div class="form-section"> <h3><i class="fas fa-lock"></i> Accesos</h3> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="edit-user-password">Contraseña (dejar en blanco para no cambiar)</label> <input type="password" id="edit-user-password" class="form-control"> </div> </div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Cambios</button> <button type="button" class="btn btn-cancel" id="cancel-edit-user-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Nuevo Proyecto --> <div id="add-project-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2 id="project-modal-title"><i class="fas fa-rocket"></i> Proyecto</h2> <button type="button" id="cancel-add-project" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="add-project-form" autocomplete="off"> <input type="hidden" id="edit-project-id" name="id"> <div class="form-section"> <h3><i class="fas fa-info-circle"></i> Información General</h3> <div class="form-group"> <label for="add-project-name">Nombre del Proyecto</label> <input type="text" id="add-project-name" name="nombre" class="form-control" required placeholder="Ej: Sistema de Facturación Inteligente"> </div> <div class="form-group"> <label for="add-project-description"><i class="fas fa-align-left"></i> Descripción</label> <textarea id="add-project-description" name="descripcion" class="form-control" required placeholder="Describe tu proyecto de forma creativa..."></textarea> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="add-project-price"><i class="fas fa-dollar-sign"></i> Precio (<span data-moneda-simbolo>$</span>)</label> <input type="number" id="add-project-price" name="precio" class="form-control" required placeholder="Ej: 1500"> </div> </div> <div class="form-col"> <div class="form-group"> <label for="add-project-features"><i class="fas fa-list"></i> Características</label> <input type="text" id="add-project-features" name="caracteristicas" class="form-control" placeholder="Ej: Consultoría, Implementación, Capacitación" required> </div> </div> </div> </div> <div class="form-section"> <h3><i class="fas fa-link"></i> Recursos</h3> <div class="form-group"> <label for="add-project-link">Link del Proyecto <span>(opcional)</span></label> <input type="url" id="add-project-link" name="link" class="form-control" placeholder="https://tuproyecto.com"> </div> <div class="form-group"> <label for="add-project-image"><i class="fas fa-image"></i> Imagen</label> <input type="file" id="add-project-image" name="imagen" class="form-control" accept="image/*"> <div class="image-preview" id="add-project-image-preview"> <i class="fas fa-cloud-upload-alt"></i> </div> </div> </div> <div class="form-actions"> <button type="submit" id="project-save-btn" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Proyecto</button> <button type="button" class="btn btn-cancel" id="cancel-add-project-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Agregar Cliente Espectacular --> <div id="add-cliente-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-user-plus"></i> Agregar Cliente</h2> <button type="button" id="cancel-add-cliente" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="add-cliente-form" autocomplete="off"> <div class="form-section"> <h3><i class="fas fa-id-card"></i> Información del Cliente</h3> <div class="form-group"> <label for="cliente-nombre">Nombre del Cliente *</label> <input type="text" id="cliente-nombre" class="form-control" placeholder="Ingresa el nombre del cliente" required> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="cliente-telefono">Teléfono *</label> <input type="tel" id="cliente-telefono" class="form-control" placeholder="Ej: +50588887777" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="cliente-documento">Documento (RUC o Cédula)</label> <input type="text" id="cliente-documento" class="form-control" placeholder="Ej: 001-120399-0000X"> </div> </div> </div> <div class="form-group"> <label for="cliente-direccion">Dirección *</label> <input type="text" id="cliente-direccion" class="form-control" placeholder="Ej: Managua, Nicaragua" required> </div> <div class="form-group"> <label for="cliente-foto">Foto</label> <input type="file" id="cliente-foto" class="form-control" accept="image/*" onchange="previsualizarLogo(this, 'cliente-foto-preview')"> <div class="image-preview" id="cliente-foto-preview"><i class="fas fa-cloud-upload-alt"></i></div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Cliente</button> <button type="button" class="btn btn-cancel" id="cancel-add-cliente-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Editar Cliente --> <div id="edit-cliente-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-user-edit"></i> Editar Cliente</h2> <button type="button" id="cancel-edit-cliente" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="edit-cliente-form" autocomplete="off"> <input type="hidden" id="edit-cliente-id"> <div class="form-section"> <h3><i class="fas fa-id-card"></i> Información del Cliente</h3> <div class="form-group"> <label for="edit-cliente-nombre">Nombre del Cliente *</label> <input type="text" id="edit-cliente-nombre" class="form-control" required> </div> <div class="form-row"> <div class="form-col"> <div class="form-group"> <label for="edit-cliente-telefono">Teléfono *</label> <input type="tel" id="edit-cliente-telefono" class="form-control" required> </div> </div> <div class="form-col"> <div class="form-group"> <label for="edit-cliente-documento">Documento (RUC o Cédula)</label> <input type="text" id="edit-cliente-documento" class="form-control"> </div> </div> </div> <div class="form-group"> <label for="edit-cliente-direccion">Dirección *</label> <input type="text" id="edit-cliente-direccion" class="form-control" required> </div> <div class="form-group"> <label for="edit-cliente-foto">Foto</label> <input type="file" id="edit-cliente-foto" class="form-control" accept="image/*" onchange="previsualizarLogo(this, 'edit-cliente-foto-preview')"> <div class="image-preview" id="edit-cliente-foto-preview"><i class="fas fa-cloud-upload-alt"></i></div> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Cambios</button> <button type="button" class="btn btn-cancel" id="cancel-edit-cliente-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <!-- Modal de Ajustes --> <div id="ajustes-modal" class="custom-modal"> <div class="modal-content venta-modal"> <div class="modal-header"> <h2><i class="fas fa-cog"></i> Ajustes</h2> <button type="button" id="cancel-ajustes" title="Cerrar"><i class="fas fa-times"></i></button> </div> <form id="ajustes-form" autocomplete="off"> <div class="form-section"> <h3><i class="fas fa-money-bill-wave"></i> Moneda</h3> <div class="form-group"> <label for="ajustes-moneda-simbolo">Símbolo de la moneda</label> <input type="text" id="ajustes-moneda-simbolo" class="form-control" required> </div> <div class="form-group"> <label for="ajustes-moneda-codigo">Código de la moneda</label> <input type="text" id="ajustes-moneda-codigo" class="form-control" required> </div> <div class="form-group"> <label for="ajustes-moneda-nombre">Nombre de la moneda</label> <input type="text" id="ajustes-moneda-nombre" class="form-control" required> </div> <div class="form-group"> <label for="ajustes-moneda-locale">Locale de la moneda</label> <input type="text" id="ajustes-moneda-locale" class="form-control" required> </div> </div> <div class="form-actions"> <button type="submit" id="ajustes-guardar-moneda" class="btn btn-primary"><i class="fas fa-save"></i> Guardar Ajustes</button> <button type="button" class="btn btn-cancel" id="cancel-ajustes-btn"><i class="fas fa-times"></i> Cancelar</button> </div> </form> </div> </div> <style> /* MODAL NUEVO PROYECTO SORPRENDENTE */ .form-text-feedback { margin-top: 4px; font-size: 0.86rem; color: #e74c3c; } </style> <script> let MONEDA_SIMBOLO = '$'; let MONEDA_CODIGO = 'USD'; let MONEDA_NOMBRE = 'Dólar estadounidense'; let MONEDA_LOCALE = 'es-ES'; async function cargarAjustesMoneda() { try { const res = await fetch('ajustes.php?action=get'); const data = await res.json(); MONEDA_SIMBOLO = data.moneda_simbolo || '$'; MONEDA_CODIGO = data.moneda_codigo || 'USD'; MONEDA_NOMBRE = data.moneda_nombre || 'Dólar estadounidense'; MONEDA_LOCALE = data.locale || 'es-ES'; } catch (e) { MONEDA_SIMBOLO = '$'; MONEDA_CODIGO = 'USD'; MONEDA_NOMBRE = 'Dólar estadounidense'; MONEDA_LOCALE = 'es-ES'; } const inputSimbolo = document.getElementById('ajustes-moneda-simbolo'); if (inputSimbolo) inputSimbolo.value = MONEDA_SIMBOLO; const inputCodigo = document.getElementById('ajustes-moneda-codigo'); if (inputCodigo) inputCodigo.value = MONEDA_CODIGO; const inputNombre = document.getElementById('ajustes-moneda-nombre'); if (inputNombre) inputNombre.value = MONEDA_NOMBRE; const inputLocale = document.getElementById('ajustes-moneda-locale'); if (inputLocale) inputLocale.value = MONEDA_LOCALE; const simbolos = document.querySelectorAll('[data-moneda-simbolo]'); simbolos.forEach(function(el) { el.textContent = MONEDA_SIMBOLO; }); } function inicializarAjustesMoneda() { const btnGuardar = document.getElementById('ajustes-guardar-moneda'); if (!btnGuardar) return; btnGuardar.addEventListener('click', async function () { const cuerpo = { moneda_simbolo: (document.getElementById('ajustes-moneda-simbolo').value || '').trim(), moneda_codigo: (document.getElementById('ajustes-moneda-codigo').value || '').trim(), moneda_nombre: (document.getElementById('ajustes-moneda-nombre').value || '').trim(), locale: (document.getElementById('ajustes-moneda-locale').value || '').trim() }; try { const res = await fetch('ajustes.php?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cuerpo) }); const data = await res.json(); if (data.success) { mostrarMensaje('Ajustes de moneda guardados correctamente.', true); await cargarAjustesMoneda(); try { cargarServicios(); } catch (e) {} try { cargarProyectos(); } catch (e) {} try { cargarCompras(); } catch (e) {} } else { mostrarMensaje(data.error || 'No se pudieron guardar los ajustes de moneda.', false); } } catch (e) { mostrarMensaje('No se pudieron guardar los ajustes de moneda.', false); } }); } async function cargarMonedasGuardadas() { const tbody = document.getElementById('ajustes-monedas-tbody'); if (!tbody) return; tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#aaa;">Cargando...</td></tr>'; try { const res = await fetch('ajustes.php?action=list_currencies'); const monedas = await res.json(); if (!Array.isArray(monedas) || !monedas.length) { tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#aaa;">No hay monedas guardadas.</td></tr>'; return; } tbody.innerHTML = ''; monedas.forEach(m => { const tr = document.createElement('tr'); const esActiva = !!m.activa; tr.innerHTML = ` <td data-label="Nombre">${m.nombre}</td> <td data-label="Símbolo">${m.simbolo}</td> <td data-label="Estado"> ${esActiva ? '<span class="badge badge-success">Moneda en uso</span>' : '<span class="badge badge-secondary">Disponible</span>'} </td> <td data-label="Acciones"> ${esActiva ? '<span style="color:#aaa;font-size:0.9rem;">No se puede eliminar mientras esté activa</span>' : `<div class="moneda-actions"> <button class="action-btn" type="button" onclick="usarMonedaGuardada(${m.id})" title="Usar moneda"><i class="fas fa-check-circle"></i></button> <button class="action-btn delete-btn" type="button" onclick="eliminarMonedaGuardada(${m.id})" title="Eliminar moneda"><i class="fas fa-trash"></i></button> </div>`} </td> `; tbody.appendChild(tr); }); } catch (e) { tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#e74c3c;">No se pudieron cargar las monedas.</td></tr>'; } } function inicializarMonedasGuardadas() { const btnAgregar = document.getElementById('ajustes-agregar-moneda'); if (!btnAgregar) return; btnAgregar.addEventListener('click', async function () { const nombreEl = document.getElementById('ajustes-nueva-moneda-nombre'); const simboloEl = document.getElementById('ajustes-nueva-moneda-simbolo'); const nombre = (nombreEl.value || '').trim(); const simbolo = (simboloEl.value || '').trim(); if (!nombre || !simbolo) { mostrarMensaje('Por favor, escribe el nombre y el símbolo de la moneda.', false); return; } try { const res = await fetch('ajustes.php?action=add_currency', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, simbolo }) }); const data = await res.json(); if (data.success) { mostrarMensaje('Moneda guardada en la lista.', true); nombreEl.value = ''; simboloEl.value = ''; await cargarMonedasGuardadas(); } else { mostrarMensaje(data.error || 'No se pudo guardar la moneda.', false); } } catch (e) { mostrarMensaje('No se pudo guardar la moneda.', false); } }); } async function usarMonedaGuardada(id) { try { const res = await fetch('ajustes.php?action=set_active', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const data = await res.json(); if (data.success) { await cargarAjustesMoneda(); try { cargarServicios(); } catch (e) {} try { cargarProyectos(); } catch (e) {} try { cargarCompras(); } catch (e) {} await cargarMonedasGuardadas(); mostrarMensaje('Moneda activa actualizada.', true); } else { mostrarMensaje(data.error || 'No se pudo activar la moneda seleccionada.', false); } } catch (e) { mostrarMensaje('No se pudo activar la moneda seleccionada.', false); } } async function eliminarMonedaGuardada(id) { if (!confirm('¿Eliminar esta moneda guardada?')) return; try { const res = await fetch('ajustes.php?action=delete_currency', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const data = await res.json(); if (data.success) { mostrarMensaje('Moneda eliminada correctamente.', true); await cargarMonedasGuardadas(); } else { mostrarMensaje(data.error || 'No se pudo eliminar la moneda.', false); } } catch (e) { mostrarMensaje('No se pudo eliminar la moneda.', false); } } // Crear partículas para el fondo function createParticles() { const particlesContainer = document.getElementById('particles'); const particleCount = 50; for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.classList.add('particle'); // Tamaño aleatorio const size = Math.random() * 15 + 5; particle.style.width = `${size}px`; particle.style.height = `${size}px`; // Posición inicial aleatoria particle.style.left = `${Math.random() * 100}%`; particle.style.top = `${Math.random() * 100}%`; // Duración de animación aleatoria const duration = Math.random() * 20 + 10; particle.style.animationDuration = `${duration}s`; // Retraso aleatorio const delay = Math.random() * 5; particle.style.animationDelay = `${delay}s`; particlesContainer.appendChild(particle); } } // Fecha actual function updateCurrentDate() { const now = new Date(); const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; // Icono de calendario animado y fecha con gradiente document.getElementById('currentDate').innerHTML = `<i class='fas fa-calendar-day'></i> <span>${now.toLocaleDateString('es-ES', options)}</span>`; } // Cambiar secciones function setupMenuNavigation() { document.querySelectorAll('.menu-item').forEach(item => { item.addEventListener('click', function() { const sectionId = this.getAttribute('data-section'); if (sectionId) { // Remover activo de todos los items document.querySelectorAll('.menu-item').forEach(i => { i.classList.remove('active'); }); // Agregar activo al item actual this.classList.add('active'); // Ocultar todas las secciones document.querySelectorAll('.section').forEach(section => { section.classList.remove('active'); }); // Mostrar sección correspondiente document.getElementById(`${sectionId}-section`).classList.add('active'); } }); }); } // Toggle sidebar en móviles mejorado function setupSidebarToggle() { const sidebar = document.querySelector('.admin-sidebar'); const toggleBtn = document.querySelector('.toggle-sidebar'); const overlay = document.getElementById('sidebar-overlay'); // Animación hamburguesa function toggleHamburger(active) { if (active) { toggleBtn.classList.add('active'); } else { toggleBtn.classList.remove('active'); } } function openSidebar() { sidebar.classList.add('active'); overlay.classList.add('active'); toggleHamburger(true); document.body.style.overflow = 'hidden'; } function closeSidebar() { sidebar.classList.remove('active'); overlay.classList.remove('active'); toggleHamburger(false); document.body.style.overflow = ''; } toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); if (sidebar.classList.contains('active')) { closeSidebar(); } else { openSidebar(); } }); overlay.addEventListener('click', function() { closeSidebar(); }); // Cerrar al seleccionar una opción del menú en móvil document.querySelectorAll('.menu-item').forEach(item => { item.addEventListener('click', function() { if (window.innerWidth <= 992) { closeSidebar(); } }); }); // Cerrar con ESC document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && sidebar.classList.contains('active')) { closeSidebar(); } }); // Cerrar al hacer clic fuera del menú y del botón hamburguesa document.addEventListener('mousedown', function(e) { if ( window.innerWidth <= 992 && sidebar.classList.contains('active') && !sidebar.contains(e.target) && !toggleBtn.contains(e.target) ) { closeSidebar(); } }); } // Vista previa de imagen function setupImagePreview() { const imageInput = document.getElementById('project-image'); const preview = document.getElementById('image-preview'); if (imageInput) { imageInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = `<img src="${e.target.result}" alt="Vista previa">`; } reader.readAsDataURL(file); } else { preview.innerHTML = '<i class="fas fa-cloud-upload-alt" style="font-size: 3rem; color: rgba(255,255,255,0.3);"></i>'; } }); } } // Modal de cierre de sesión bonito y responsive function setupLogoutConfirmation() { const logoutBtn = document.getElementById('logout-btn'); const logoutModal = document.getElementById('logout-modal'); const goodbyeModal = document.getElementById('goodbye-modal'); const confirmBtn = document.getElementById('confirm-logout'); const cancelBtn = document.getElementById('cancel-logout'); if (logoutBtn && logoutModal && confirmBtn && cancelBtn && goodbyeModal) { logoutBtn.addEventListener('click', function(e) { e.preventDefault(); logoutModal.classList.add('active'); }); cancelBtn.addEventListener('click', function() { logoutModal.classList.remove('active'); }); confirmBtn.addEventListener('click', function() { logoutModal.classList.remove('active'); goodbyeModal.classList.add('active'); setTimeout(() => { goodbyeModal.classList.remove('active'); window.location.href = 'logout.php'; }, 1800); // 1.8 segundos de despedida }); // Cerrar modal si se hace click fuera del contenido [logoutModal, goodbyeModal].forEach(modal => { modal.addEventListener('click', function(e) { if (e.target === modal && modal === logoutModal) { logoutModal.classList.remove('active'); } }); }); } } // Inicializar todo document.addEventListener('DOMContentLoaded', function() { createParticles(); updateCurrentDate(); setupMenuNavigation(); setupSidebarToggle(); setupImagePreview(); setupLogoutConfirmation(); cargarAjustesMoneda(); inicializarAjustesMoneda(); cargarMonedasGuardadas(); inicializarMonedasGuardadas(); actualizarServiciosActivos(); // <-- Aquí actualizarProyectosCompletados(); // <-- Agrega esto actualizarUsuariosTotales(); // <-- NUEVO // Mostrar la fecha actual cada minuto setInterval(updateCurrentDate, 60000); }); // --- GESTIÓN DE SERVICIOS DINÁMICA --- function cargarServicios() { fetch('servicios.php?action=list') .then(res => res.json()) .then(data => { const tbody = document.getElementById('services-table-body'); const cardsContainer = document.getElementById('services-cards-container'); tbody.innerHTML = ''; cardsContainer.innerHTML = ''; if (data.length === 0) { tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#aaa;">No hay servicios registrados.</td></tr>'; cardsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:18px 0;">No hay servicios registrados.</div>'; return; } data.forEach(servicio => { // Tabla (desktop) tbody.innerHTML += ` <tr> <td>${servicio.nombre}</td> <td>${servicio.descripcion}</td> <td>${servicio.categoria}</td> <td>${formatearMoneda(servicio.precio)}</td> <td> <div class="acciones-inline"> <button class="action-btn edit-btn" onclick="editarServicio(${servicio.id})" title="Editar servicio"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarServicio(${servicio.id})" title="Eliminar servicio"><i class="fas fa-trash"></i></button> </div> </td> </tr> `; // Tarjeta (móvil) const card = document.createElement('div'); card.className = 'service-card'; card.innerHTML = ` <div class="service-card-title">${servicio.nombre}</div> <div class="service-card-desc">${servicio.descripcion}</div> <div class="service-card-meta"> <span><b>Precio:</b> ${formatearMoneda(servicio.precio)}</span> <span><b>Categoría:</b> ${servicio.categoria}</span> </div> <div class="service-card-actions"> <button class="action-btn edit-btn" onclick="editarServicio(${servicio.id})" title="Editar servicio"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarServicio(${servicio.id})" title="Eliminar servicio"><i class="fas fa-trash"></i></button> </div> `; cardsContainer.appendChild(card); }); }); } // Guardar servicio const serviceForm = document.getElementById('service-form'); if (serviceForm) { serviceForm.addEventListener('submit', function(e) { e.preventDefault(); const nombre = document.getElementById('service-name').value.trim(); const descripcion = document.getElementById('service-description').value.trim(); const precio = document.getElementById('service-price').value.trim(); const categoria = document.getElementById('service-category').value.trim(); if (!nombre || !descripcion || !precio || !categoria) { mostrarMensaje('Por favor, completa todos los campos.', false); return; } fetch('servicios.php?action=add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, descripcion, precio, categoria }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('¡Servicio agregado exitosamente!', true); cargarServicios(); serviceForm.reset(); } else { mostrarMensaje(data.error || 'Error al agregar el servicio.', false); } }); }); } // Eliminar servicio function eliminarServicio(id) { if (!confirm('¿Seguro que deseas eliminar este servicio?')) return; fetch('servicios.php?action=delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Servicio eliminado.', true); cargarServicios(); actualizarServiciosActivos(); // <-- Aquí } else { mostrarMensaje(data.error || 'Error al eliminar.', false); } }); } // Mensaje bonito function mostrarMensaje(msg, exito) { let div = document.getElementById('mensaje-servicio'); if (!div) { div = document.createElement('div'); div.id = 'mensaje-servicio'; div.style.position = 'fixed'; div.style.top = '30px'; div.style.right = '30px'; div.style.zIndex = 9999; div.style.padding = '18px 32px'; div.style.borderRadius = '14px'; div.style.fontWeight = '700'; div.style.fontSize = '1.1rem'; div.style.boxShadow = '0 4px 18px rgba(44,62,80,0.13)'; div.style.marginBottom = '20px'; // Espacio extra document.body.appendChild(div); } div.innerHTML = (exito ? '✅ ' : '❌ ') + msg; div.style.background = exito ? 'linear-gradient(90deg,#25d366,#128c7e)' : 'linear-gradient(90deg,#e74c3c,#c0392b)'; div.style.color = '#fff'; div.style.display = 'block'; setTimeout(() => { div.style.display = 'none'; }, 2500); } // Inicializar servicios al cargar la sección if (document.getElementById('services-section')) { cargarServicios(); } // --- MODAL DE EDICIÓN DE SERVICIO --- function editarServicio(id) { fetch('servicios.php?action=get&id=' + id) .then(res => res.json()) .then(data => { if (!data.id) { mostrarMensaje('No se pudo cargar el servicio.', false); return; } document.getElementById('edit-service-id').value = data.id; document.getElementById('edit-service-name').value = data.nombre; document.getElementById('edit-service-description').value = data.descripcion; document.getElementById('edit-service-price').value = data.precio; document.getElementById('edit-service-category').value = data.categoria; document.getElementById('edit-service-modal').classList.add('active'); document.body.style.overflow = 'hidden'; // Bloquear scroll }); } // Cerrar modal de edición const cancelEditServiceHeaderBtn = document.getElementById('cancel-edit-service'); const cancelEditServiceFooterBtn = document.getElementById('cancel-edit-service-btn'); function cerrarModalEditarServicio() { document.getElementById('edit-service-modal').classList.remove('active'); document.body.style.overflow = ''; } if (cancelEditServiceHeaderBtn) { cancelEditServiceHeaderBtn.onclick = cerrarModalEditarServicio; } if (cancelEditServiceFooterBtn) { cancelEditServiceFooterBtn.onclick = cerrarModalEditarServicio; } // Cerrar modal si se hace click fuera del contenido const editServiceModal = document.getElementById('edit-service-modal'); if (editServiceModal) { editServiceModal.addEventListener('click', function(e) { if (e.target === editServiceModal) { editServiceModal.classList.remove('active'); document.body.style.overflow = ''; } }); } // Guardar cambios de edición const editServiceForm = document.getElementById('edit-service-form'); if (editServiceForm) { editServiceForm.addEventListener('submit', function(e) { e.preventDefault(); const id = document.getElementById('edit-service-id').value; const nombre = document.getElementById('edit-service-name').value.trim(); const descripcion = document.getElementById('edit-service-description').value.trim(); const precio = document.getElementById('edit-service-price').value.trim(); const categoria = document.getElementById('edit-service-category').value.trim(); if (!nombre || !descripcion || !precio || !categoria) { mostrarMensaje('Por favor, completa todos los campos.', false); return; } fetch('servicios.php?action=edit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, nombre, descripcion, precio, categoria }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('¡Servicio actualizado!', true); document.getElementById('edit-service-modal').classList.remove('active'); document.body.style.overflow = ''; cargarServicios(); actualizarServiciosActivos(); // <-- Aquí } else { mostrarMensaje(data.error || 'Error al actualizar.', false); } }); }); } // --- MODAL DE NUEVO SERVICIO --- const addServiceBtn = document.getElementById('add-service'); const addServiceModal = document.getElementById('add-service-modal'); const cancelAddServiceHeaderBtn = document.getElementById('cancel-add-service'); const cancelAddServiceFooterBtn = document.getElementById('cancel-add-service-btn'); const addServiceForm = document.getElementById('add-service-form'); if (addServiceBtn && addServiceModal) { addServiceBtn.onclick = function() { addServiceModal.classList.add('active'); document.body.style.overflow = 'hidden'; // Limpiar campos addServiceForm.reset(); }; } function cerrarModalAgregarServicio() { addServiceModal.classList.remove('active'); document.body.style.overflow = ''; addServiceForm.reset(); } if (cancelAddServiceHeaderBtn) { cancelAddServiceHeaderBtn.onclick = cerrarModalAgregarServicio; } if (cancelAddServiceFooterBtn) { cancelAddServiceFooterBtn.onclick = cerrarModalAgregarServicio; } if (addServiceModal) { addServiceModal.addEventListener('click', function(e) { if (e.target === addServiceModal) { cerrarModalAgregarServicio(); } }); } if (addServiceForm) { addServiceForm.addEventListener('submit', function(e) { e.preventDefault(); const nombre = document.getElementById('add-service-name').value.trim(); const descripcion = document.getElementById('add-service-description').value.trim(); const precio = document.getElementById('add-service-price').value.trim(); const categoria = document.getElementById('add-service-category').value.trim(); if (!nombre || !descripcion || !precio || !categoria) { mostrarMensaje('Por favor, completa todos los campos.', false); return; } fetch('servicios.php?action=add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, descripcion, precio, categoria }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('¡Servicio agregado exitosamente!', true); addServiceModal.classList.remove('active'); document.body.style.overflow = ''; cargarServicios(); actualizarServiciosActivos(); // <-- Aquí addServiceForm.reset(); } else { mostrarMensaje(data.error || 'Error al agregar el servicio.', false); } }); }); } // --- GESTIÓN DE PROYECTOS DINÁMICA --- const addProjectBtn = document.getElementById('add-project'); const addProjectModal = document.getElementById('add-project-modal'); const cancelAddProjectHeaderBtn = document.getElementById('cancel-add-project'); const cancelAddProjectFooterBtn = document.getElementById('cancel-add-project-btn'); const addProjectForm = document.getElementById('add-project-form'); const addProjectImageInput = document.getElementById('add-project-image'); const addProjectImagePreview = document.getElementById('add-project-image-preview'); if (addProjectBtn && addProjectModal) { addProjectBtn.onclick = function() { addProjectModal.classList.add('active'); document.body.style.overflow = 'hidden'; addProjectForm.reset(); addProjectForm.removeAttribute('data-edit-id'); addProjectImagePreview.innerHTML = '<i class="fas fa-cloud-upload-alt"></i>'; }; } // Unificar el submit para agregar y editar if (addProjectForm) { addProjectForm.onsubmit = function(e) { e.preventDefault(); const editId = document.getElementById('edit-project-id').value; const imagenInput = document.getElementById('add-project-image'); // Si es nuevo proyecto, la imagen es obligatoria if (!editId && (!imagenInput.files || !imagenInput.files.length)) { mostrarMensaje('Por favor, selecciona una imagen para el proyecto.', false); imagenInput.focus(); return; } const formData = new FormData(addProjectForm); let url = 'proyectos.php?action=add'; if (editId) { url = 'proyectos.php?action=edit'; formData.append('id', editId); } fetch(url, { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje(editId ? '¡Proyecto actualizado!' : '¡Proyecto agregado exitosamente!', true); addProjectModal.classList.remove('active'); document.body.style.overflow = ''; addProjectForm.reset(); addProjectImagePreview.innerHTML = '<i class="fas fa-cloud-upload-alt" style="font-size: 1rem; color: rgba(255,255,255,0.3);"></i>'; document.getElementById('project-modal-title').textContent = 'Nuevo Proyecto'; document.getElementById('project-save-btn').innerHTML = '<i class="fas fa-save"></i> Guardar Proyecto'; document.getElementById('edit-project-id').value = ''; cargarProyectos(); } else { mostrarMensaje(data.error || 'Error al guardar el proyecto.', false); } }); }; } function cerrarModalProyecto() { addProjectModal.classList.remove('active'); document.body.style.overflow = ''; addProjectForm.removeAttribute('data-edit-id'); addProjectForm.reset(); addProjectImagePreview.innerHTML = '<i class="fas fa-cloud-upload-alt"></i>'; } if (cancelAddProjectHeaderBtn) { cancelAddProjectHeaderBtn.onclick = cerrarModalProyecto; } if (cancelAddProjectFooterBtn) { cancelAddProjectFooterBtn.onclick = cerrarModalProyecto; } if (addProjectModal) { addProjectModal.addEventListener('click', function(e) { if (e.target === addProjectModal) { cerrarModalProyecto(); } }); } // FUNCIÓN PARA ACTUALIZAR EL NÚMERO DE SERVICIOS ACTIVOS EN EL DASHBOARD function actualizarServiciosActivos() { fetch('servicios.php?action=count') .then(res => res.json()) .then(data => { document.getElementById('servicios-activos-num').textContent = data.total; }); } // FUNCIÓN PARA CARGAR PROYECTOS Y MOSTRARLOS EN LA TABLA function cargarProyectos() { fetch('proyectos.php?action=list') .then(res => res.json()) .then(data => { const tbody = document.getElementById('projects-table-body'); const cardsContainer = document.getElementById('projects-cards-container'); tbody.innerHTML = ''; cardsContainer.innerHTML = ''; if (!data.length) { tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#aaa;">No hay proyectos registrados.</td></tr>'; cardsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:18px 0;">No hay proyectos registrados.</div>'; return; } data.forEach(proyecto => { // Tabla (desktop) tbody.innerHTML += ` <tr> <td style="text-align:center;"> ${proyecto.imagen ? `<img src="${proyecto.imagen}" alt="Imagen" style="max-width:60px;max-height:40px;border-radius:8px;box-shadow:0 2px 8px #27ae6033;">` : '<span style="color:#bbb;font-size:1.2rem;">—</span>'} </td> <td><b>${proyecto.nombre}</b></td> <td>${proyecto.descripcion}</td> <td>${proyecto.caracteristicas}</td> <td><span style="color:#27ae60;font-weight:700;">${formatearMoneda(proyecto.precio)}</span></td> <td> <div class="acciones-inline"> <button class="action-btn edit-btn" onclick="editarProyecto(${proyecto.id})" title="Editar proyecto"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarProyecto(${proyecto.id})" title="Eliminar proyecto"><i class="fas fa-trash"></i></button> ${proyecto.link ? `<a href="${proyecto.link}" target="_blank" class="action-btn" style="background:linear-gradient(90deg,#27ae60,#3498db);color:#fff;" title="Ver proyecto"><i class='fas fa-rocket'></i></a>` : ''} </div> </td> </tr> `; // Tarjeta (móvil) const card = document.createElement('div'); card.className = 'project-card'; card.innerHTML = ` <div class="project-card-header"> ${proyecto.imagen ? `<img src="${proyecto.imagen}" class="project-card-img" alt="Imagen">` : '<div class="project-card-img" style="background:#232946;color:#bbb;display:flex;align-items:center;justify-content:center;font-size:1.3rem;">—</div>'} <div class="project-card-title">${proyecto.nombre}</div> </div> <div class="project-card-desc">${proyecto.descripcion}</div> <div class="project-card-meta"> <span><b>Características:</b> ${proyecto.caracteristicas}</span> <span class="project-card-price"><b>Precio:</b> ${formatearMoneda(proyecto.precio)}</span> </div> <div class="project-card-actions"> <button class="action-btn edit-btn" onclick="editarProyecto(${proyecto.id})" title="Editar proyecto"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarProyecto(${proyecto.id})" title="Eliminar proyecto"><i class="fas fa-trash"></i></button> ${proyecto.link ? `<a href="${proyecto.link}" target="_blank" class="action-btn" style="background:linear-gradient(90deg,#27ae60,#3498db);color:#fff;" title="Ver proyecto"><i class='fas fa-rocket'></i></a>` : ''} </div> `; cardsContainer.appendChild(card); }); }); } // Llamar a cargarProyectos al cargar la sección de proyectos if (document.getElementById('projects-section')) { cargarProyectos(); } // --- FUNCIONES DE ACCIÓN PARA PROYECTOS (opcional: puedes implementar editar/eliminar si lo deseas) --- function eliminarProyecto(id) { if (!confirm('¿Seguro que deseas eliminar este proyecto?')) return; fetch('proyectos.php?action=delete', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'id=' + encodeURIComponent(id) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Proyecto eliminado.', true); cargarProyectos(); } else { mostrarMensaje(data.error || 'Error al eliminar el proyecto.', false); } }); } function editarProyecto(id) { fetch('proyectos.php?action=get&id=' + id) .then(res => res.json()) .then(data => { if (!data.id) { mostrarMensaje('No se pudo cargar el proyecto.', false); return; } document.getElementById('edit-project-id').value = data.id; document.getElementById('add-project-name').value = data.nombre; document.getElementById('add-project-description').value = data.descripcion; document.getElementById('add-project-price').value = data.precio; document.getElementById('add-project-features').value = data.caracteristicas; document.getElementById('add-project-link').value = data.link || ''; // Previsualización de imagen const preview = document.getElementById('add-project-image-preview'); if (data.imagen) { preview.innerHTML = `<img src="${data.imagen}" alt="Vista previa" style="max-width:70px;max-height:36px;border-radius:6px;box-shadow:0 2px 6px #27ae6033;">`; } else { preview.innerHTML = '<i class="fas fa-cloud-upload-alt" style="font-size: 1rem; color: rgba(255,255,255,0.3);"></i>'; } document.getElementById('add-project-image').required = false; document.getElementById('project-modal-title').textContent = 'Editar Proyecto'; document.getElementById('project-save-btn').innerHTML = '<i class="fas fa-save"></i> Guardar Cambios'; document.getElementById('add-project-modal').classList.add('active'); document.body.style.overflow = 'hidden'; }); } // Modificar el submit para agregar/editar if (addProjectForm) { addProjectForm.onsubmit = function(e) { e.preventDefault(); const editId = document.getElementById('edit-project-id').value; const imagenInput = document.getElementById('add-project-image'); // Si es nuevo proyecto, la imagen es obligatoria if (!editId && (!imagenInput.files || !imagenInput.files.length)) { mostrarMensaje('Por favor, selecciona una imagen para el proyecto.', false); imagenInput.focus(); return; } const formData = new FormData(addProjectForm); let url = 'proyectos.php?action=add'; if (editId) { url = 'proyectos.php?action=edit'; formData.append('id', editId); } formData.append('link', document.getElementById('add-project-link').value.trim()); fetch(url, { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje(editId ? '¡Proyecto actualizado!' : '¡Proyecto agregado exitosamente!', true); addProjectModal.classList.remove('active'); document.body.style.overflow = ''; addProjectForm.reset(); addProjectImagePreview.innerHTML = '<i class="fas fa-cloud-upload-alt" style="font-size: 1rem; color: rgba(255,255,255,0.3);"></i>'; document.getElementById('project-modal-title').textContent = 'Nuevo Proyecto'; document.getElementById('project-save-btn').innerHTML = '<i class="fas fa-save"></i> Guardar Proyecto'; document.getElementById('edit-project-id').value = ''; cargarProyectos(); } else { mostrarMensaje(data.error || 'Error al guardar el proyecto.', false); } }); }; } // Al abrir el modal para nuevo proyecto, limpiar todo if (addProjectBtn && addProjectModal) { addProjectBtn.onclick = function() { addProjectModal.classList.add('active'); document.body.style.overflow = 'hidden'; addProjectForm.reset(); addProjectForm.removeAttribute('data-edit-id'); addProjectImagePreview.innerHTML = '<i class="fas fa-cloud-upload-alt" style="font-size: 2.2rem; color: rgba(255,255,255,0.3);"></i>'; document.getElementById('project-modal-title').textContent = 'Nuevo Proyecto'; document.getElementById('project-save-btn').innerHTML = '<i class="fas fa-save"></i> Guardar Proyecto'; document.getElementById('edit-project-id').value = ''; document.getElementById('add-project-image').required = true; }; } // FUNCIÓN PARA ACTUALIZAR EL NÚMERO DE PROYECTOS EN EL DASHBOARD function actualizarProyectosCompletados() { fetch('proyectos.php?action=count') .then(res => res.json()) .then(data => { document.getElementById('proyectos-completados-num').textContent = data.total; }); } // --- GESTIÓN DE USUARIOS DINÁMICA --- function renderUsuariosResponsive(data) { const container = document.getElementById('users-cards-responsive'); if (!container) return; container.innerHTML = ''; if (!data.length) { container.innerHTML = '<div style="color:#aaa;text-align:center;padding:18px 0;">No hay usuarios registrados.</div>'; return; } data.forEach(usuario => { const card = document.createElement('div'); card.className = 'user-card-responsive'; // Avatar con inicial const inicial = usuario.nombre ? usuario.nombre.charAt(0).toUpperCase() : '?'; card.innerHTML = ` <div class="user-card-avatar">${inicial}</div> <div class="user-card-title"><i class='fas fa-user'></i> ${usuario.nombre}</div> <div class="user-card-meta"><span class="user-card-label"><i class='fas fa-envelope'></i> Correo:</span> <span class="user-card-value">${usuario.email}</span></div> <div class="user-card-meta"><span class="user-card-label"><i class='fas fa-user-tag'></i> Usuario:</span> <span class="user-card-value">${usuario.usuario}</span></div> <div class="user-card-actions"> <button class="action-btn edit-btn" onclick="editarUsuario(${usuario.id})" title="Editar usuario"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarUsuario(${usuario.id})" title="Eliminar usuario"><i class="fas fa-trash"></i></button> </div> `; container.appendChild(card); }); } function cargarUsuarios() { fetch('usuarios.php?action=list') .then(res => res.json()) .then(data => { const tbody = document.getElementById('users-table-body'); tbody.innerHTML = ''; if (!data.length) { tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#aaa;">No hay usuarios registrados.</td></tr>'; } else { data.forEach(usuario => { tbody.innerHTML += ` <tr> <td>${usuario.nombre}</td> <td>${usuario.email}</td> <td>${usuario.usuario}</td> <td> <div class="acciones-inline"> <button class="action-btn edit-btn" onclick="editarUsuario(${usuario.id})" title="Editar usuario"><i class="fas fa-edit"></i></button> <button class="action-btn delete-btn" onclick="eliminarUsuario(${usuario.id})" title="Eliminar usuario"><i class="fas fa-trash"></i></button> </div> </td> </tr> `; }); } renderUsuariosResponsive(data); actualizarUsuariosTotales(); // <-- NUEVO }); } // --- MODAL NUEVO USUARIO --- const addUserBtn = document.getElementById('add-user'); const addUserModal = document.getElementById('add-user-modal'); const cancelAddUserHeaderBtn = document.getElementById('cancel-add-user'); const addUserForm = document.getElementById('add-user-form'); const btnSaveUser = document.getElementById('btn-save-user'); // Validación en tiempo real function validarUsuarioForm() { let valid = true; // Nombre const nombre = document.getElementById('add-user-nombre'); const feedbackNombre = document.getElementById('feedback-nombre'); if (nombre.value.trim().length < 3) { feedbackNombre.textContent = 'Mínimo 3 caracteres.'; nombre.classList.add('error'); nombre.classList.remove('valid'); valid = false; } else { feedbackNombre.textContent = '¡Perfecto!'; feedbackNombre.classList.add('valid'); nombre.classList.remove('error'); nombre.classList.add('valid'); } // Email const email = document.getElementById('add-user-email'); const feedbackEmail = document.getElementById('feedback-email'); const emailRegex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; if (!emailRegex.test(email.value.trim())) { feedbackEmail.textContent = 'Email inválido.'; email.classList.add('error'); email.classList.remove('valid'); valid = false; } else { feedbackEmail.textContent = '¡Email válido!'; feedbackEmail.classList.add('valid'); email.classList.remove('error'); email.classList.add('valid'); } // Usuario const usuario = document.getElementById('add-user-usuario'); const feedbackUsuario = document.getElementById('feedback-usuario'); if (usuario.value.trim().length < 3) { feedbackUsuario.textContent = 'Mínimo 3 caracteres.'; usuario.classList.add('error'); usuario.classList.remove('valid'); valid = false; } else { feedbackUsuario.textContent = '¡Usuario válido!'; feedbackUsuario.classList.add('valid'); usuario.classList.remove('error'); usuario.classList.add('valid'); } // Contraseña const password = document.getElementById('add-user-password'); const feedbackPassword = document.getElementById('feedback-password'); if (password.value.length < 6) { feedbackPassword.textContent = 'Mínimo 6 caracteres.'; password.classList.add('error'); password.classList.remove('valid'); valid = false; } else { feedbackPassword.textContent = '¡Contraseña segura!'; feedbackPassword.classList.add('valid'); password.classList.remove('error'); password.classList.add('valid'); } // Confirmar contraseña const password2 = document.getElementById('add-user-password2'); const feedbackPassword2 = document.getElementById('feedback-password2'); if (password2.value !== password.value || password2.value.length < 6) { feedbackPassword2.textContent = 'Las contraseñas no coinciden.'; password2.classList.add('error'); password2.classList.remove('valid'); valid = false; } else { feedbackPassword2.textContent = '¡Coinciden!'; feedbackPassword2.classList.add('valid'); password2.classList.remove('error'); password2.classList.add('valid'); } btnSaveUser.disabled = !valid; return valid; } ['add-user-nombre','add-user-email','add-user-usuario','add-user-password','add-user-password2'].forEach(id => { const el = document.getElementById(id); el.addEventListener('input', validarUsuarioForm); }); if (addUserBtn && addUserModal) { addUserBtn.onclick = function() { addUserModal.classList.add('active'); document.body.style.overflow = 'hidden'; addUserForm.reset(); // Limpiar feedback y estilos ['add-user-nombre','add-user-email','add-user-usuario','add-user-password','add-user-password2'].forEach(id => { document.getElementById(id).classList.remove('error','valid'); }); ['feedback-nombre','feedback-email','feedback-usuario','feedback-password','feedback-password2'].forEach(id => { document.getElementById(id).textContent = ''; document.getElementById(id).classList.remove('valid'); }); btnSaveUser.disabled = true; }; } const cancelAddUserFooterBtn = document.getElementById('cancel-add-user-btn'); function cerrarModalAgregarUsuario() { addUserModal.classList.remove('active'); document.body.style.overflow = ''; } if (cancelAddUserHeaderBtn) { cancelAddUserHeaderBtn.onclick = cerrarModalAgregarUsuario; } if (cancelAddUserFooterBtn) { cancelAddUserFooterBtn.onclick = cerrarModalAgregarUsuario; } if (addUserModal) { addUserModal.addEventListener('click', function(e) { if (e.target === addUserModal) { addUserModal.classList.remove('active'); document.body.style.overflow = ''; } }); } if (addUserForm) { addUserForm.addEventListener('submit', function(e) { e.preventDefault(); if (!validarUsuarioForm()) return; const nombre = document.getElementById('add-user-nombre').value.trim(); const email = document.getElementById('add-user-email').value.trim(); const usuario = document.getElementById('add-user-usuario').value.trim(); const password = document.getElementById('add-user-password').value; fetch('usuarios.php?action=add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, email, usuario, password }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('¡Usuario agregado exitosamente!', true); addUserModal.classList.remove('active'); document.body.style.overflow = ''; cargarUsuarios(); addUserForm.reset(); actualizarUsuariosTotales(); // <-- NUEVO } else { mostrarMensaje(data.error || 'Error al agregar el usuario.', false); } }); }); } // --- MODAL EDICIÓN USUARIO --- function editarUsuario(id) { fetch('usuarios.php?action=get&id=' + id) .then(res => res.json()) .then(data => { if (!data.id) { mostrarMensaje('No se pudo cargar el usuario.', false); return; } document.getElementById('edit-user-id').value = data.id; document.getElementById('edit-user-nombre').value = data.nombre; document.getElementById('edit-user-email').value = data.email; document.getElementById('edit-user-usuario').value = data.usuario; document.getElementById('edit-user-password').value = ''; document.getElementById('edit-user-modal').classList.add('active'); document.body.style.overflow = 'hidden'; }); } const cancelEditUserHeaderBtn = document.getElementById('cancel-edit-user'); const cancelEditUserFooterBtn = document.getElementById('cancel-edit-user-btn'); function cerrarModalEditarUsuario() { document.getElementById('edit-user-modal').classList.remove('active'); document.body.style.overflow = ''; } if (cancelEditUserHeaderBtn) { cancelEditUserHeaderBtn.onclick = cerrarModalEditarUsuario; } if (cancelEditUserFooterBtn) { cancelEditUserFooterBtn.onclick = cerrarModalEditarUsuario; } const editUserModal = document.getElementById('edit-user-modal'); if (editUserModal) { editUserModal.addEventListener('click', function(e) { if (e.target === editUserModal) { editUserModal.classList.remove('active'); document.body.style.overflow = ''; } }); } const editUserForm = document.getElementById('edit-user-form'); if (editUserForm) { editUserForm.addEventListener('submit', function(e) { e.preventDefault(); const id = document.getElementById('edit-user-id').value; const nombre = document.getElementById('edit-user-nombre').value.trim(); const email = document.getElementById('edit-user-email').value.trim(); const usuario = document.getElementById('edit-user-usuario').value.trim(); const password = document.getElementById('edit-user-password').value; if (!nombre || !email || !usuario) { mostrarMensaje('Por favor, completa todos los campos.', false); return; } fetch('usuarios.php?action=edit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, nombre, email, usuario, password }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('¡Usuario actualizado!', true); document.getElementById('edit-user-modal').classList.remove('active'); document.body.style.overflow = ''; cargarUsuarios(); actualizarUsuariosTotales(); // <-- NUEVO } else { mostrarMensaje(data.error || 'Error al actualizar.', false); } }); }); } // --- ELIMINAR USUARIO --- function eliminarUsuario(id) { if (!confirm('¿Seguro que deseas eliminar este usuario?')) return; fetch('usuarios.php?action=delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Usuario eliminado.', true); cargarUsuarios(); actualizarUsuariosTotales(); // <-- NUEVO } else { mostrarMensaje(data.error || 'Error al eliminar.', false); } }); } // Inicializar usuarios al cargar la sección if (document.getElementById('users-section')) { cargarUsuarios(); } // FUNCIÓN PARA ACTUALIZAR EL NÚMERO DE USUARIOS EN EL DASHBOARD function actualizarUsuariosTotales() { fetch('usuarios.php?action=count') .then(res => res.json()) .then(data => { document.querySelector('.stat-card .fa-users').closest('.stat-card').querySelector('h3').textContent = data.total; }); } // --- GESTIÓN DE COMPRAS DINÁMICA --- function cargarCompras() { fetch('proyectos.php?action=compras') .then(res => res.json()) .then(data => { const cardsContainer = document.getElementById('compras-cards-container'); cardsContainer.innerHTML = ''; if (!data.length) { cardsContainer.innerHTML = '<div class="compras-empty">No hay ventas registradas.</div>'; return; } data.forEach(compra => { const items = Array.isArray(compra.items) ? compra.items : null; const totalVenta = (items ? Number(compra.total) : Number(compra.precio)) || 0; const precioFormateado = formatearMoneda(totalVenta); let tipoTexto = ''; let tipoIcono = 'fas fa-file-invoice'; let tipoClase = ''; let itemsHtml = ''; const clienteRegistrado = Number(compra.cliente_id || 0) > 0; let imagenVenta = clienteRegistrado ? (compra.cliente_foto || null) : null; if (items && items.length > 0) { const tipos = items.map(it => (it.tipo || '').trim()).filter(Boolean); const unico = tipos.length > 0 && tipos.every(t => t === tipos[0]) ? tipos[0] : ''; if (unico === 'proyecto') { tipoTexto = 'Proyecto'; tipoIcono = 'fas fa-rocket'; tipoClase = 'tipo-proyecto'; } else if (unico === 'servicio') { tipoTexto = 'Servicio'; tipoIcono = 'fas fa-briefcase'; tipoClase = 'tipo-servicio'; } else { tipoTexto = 'Mixto'; tipoIcono = 'fas fa-layer-group'; tipoClase = 'tipo-servicio'; } const list = items.map(it => { const n = it.item_nombre || ''; const link = (it.tipo === 'proyecto' && it.link) ? `<a class="compra-item-link" href="${it.link}" target="_blank">${n}</a>` : `<span class="compra-item-text">${n}</span>`; return `<div style="margin-top:4px;">${link}</div>`; }).join(''); itemsHtml = `<div>${list}</div>`; const imgItem = items.find(it => it && it.imagen); if (clienteRegistrado && !imagenVenta && imgItem && imgItem.imagen) imagenVenta = imgItem.imagen; } else { const itemLink = compra.tipo === 'proyecto' && compra.link ? `<a class="compra-item-link" href="${compra.link}" target="_blank">${compra.item_nombre}</a>` : `<span class="compra-item-text">${compra.item_nombre}</span>`; itemsHtml = itemLink; tipoTexto = compra.tipo === 'proyecto' ? 'Proyecto' : 'Servicio'; tipoIcono = compra.tipo === 'proyecto' ? 'fas fa-rocket' : 'fas fa-briefcase'; tipoClase = compra.tipo === 'proyecto' ? 'tipo-proyecto' : 'tipo-servicio'; } const whatsappBtn = compra.telefono ? `<a class="compra-whatsapp" href="https://wa.me/58${compra.telefono.replace(/[^0-9]/g, '')}" target="_blank" title="Contactar por WhatsApp"><i class="fab fa-whatsapp"></i></a>` : ''; // Formatear solo la fecha (sin hora) const fechaSolo = new Date(compra.fecha).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }); const card = document.createElement('div'); card.className = 'service-card compra-card'; card.innerHTML = ` <div class="compra-card-body"> <div class="compra-card-left"> <div class="compra-card-header"> <div class="compra-avatar">${imagenVenta ? `<img src='${imagenVenta}' alt='Cliente'>` : `<i class='fas fa-user'></i>`}</div> <div class="compra-card-title"> <span class="compra-label"><i class='fas fa-id-card'></i> Nombre del Cliente:</span> ${compra.nombre} </div> </div> <div class="compra-card-meta"> <span><i class='fas fa-phone-alt'></i> ${compra.telefono || '-'}${whatsappBtn}</span> <span><i class='fas fa-map-marker-alt'></i> ${compra.direccion || '-'}</span> </div> <div class="compra-card-proyecto"> <div class="compra-proyecto-icon"> <i class='${tipoIcono} tipo-icon ${tipoClase}'></i> </div> <div class="compra-proyecto-detalle"> <span class="compra-proyecto-tipo">${tipoTexto}</span> ${itemsHtml} </div> </div> </div> <div class="compra-card-right"> <div class="compra-card-precio"> <span class="compra-precio-label">Monto</span> <span class="compra-precio-monto">${precioFormateado}</span> </div> <div class="compra-card-fecha"> <i class='fas fa-calendar-alt'></i> ${fechaSolo} </div> <div class="compra-card-actions"> <div class="acciones-inline"> <button class="action-btn edit-btn" onclick="editarCompra(${(compra.primer_item_id || compra.id)})" title="Editar venta"><i class="fas fa-edit"></i></button> <button class="action-btn invoice-btn" type="button" title="Generar factura"><i class="fas fa-file-invoice"></i></button> <button class="action-btn print-btn" type="button" title="Imprimir factura"><i class="fas fa-print"></i></button> <button class="action-btn delete-btn" onclick="eliminarCompra(${compra.id})" title="Eliminar venta"><i class="fas fa-trash"></i></button> </div> </div> </div> </div> `; const facturaBtn = card.querySelector('.invoice-btn'); if (facturaBtn) { facturaBtn.addEventListener('click', async () => { try { await generarFactura(compra); mostrarMensaje('Factura generada y descargada.', true); } catch (error) { console.error('No se pudo generar la factura:', error); mostrarMensaje('No se pudo generar la factura de esta venta.', false); } }); } const printBtn = card.querySelector('.print-btn'); if (printBtn) { printBtn.addEventListener('click', async (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch (_) {} if (printBtn.disabled) return; printBtn.disabled = true; try { let ventanaPreAbierta = null; try { const esResponsiveMovil = (window.matchMedia && window.matchMedia('(max-width: 768px)').matches); if (esResponsiveMovil) { ventanaPreAbierta = window.open('about:blank', '_blank'); } } catch (_) { ventanaPreAbierta = null; } await imprimirFactura(compra, ventanaPreAbierta); } catch (error) { console.error('No se pudo imprimir la factura:', error); mostrarMensaje('No se pudo imprimir la factura de esta venta.', false); } finally { setTimeout(() => { try { printBtn.disabled = false; } catch (_) {} }, 1200); } }); } cardsContainer.appendChild(card); }); }); } const DATOS_VENDEDOR = { nombre: 'Francisco Ramírez Lazo', telefono: '+505 5866 8096', correo: 'franciscoramirezlazo776@gmail.com', direccion: 'Calle del Cementerio, 100mt sur, Río San Juan, Nicaragua' }; let clientesRegistradosCache = []; function normalizarTexto(valor) { if (valor === null || valor === undefined) return ''; return String(valor).trim(); } function rellenarDatosClientePorId(clienteId, prefix) { const id = parseInt(clienteId || 0, 10); const inputNombre = document.getElementById(prefix + '-nombre'); const inputTelefono = document.getElementById(prefix + '-telefono'); const inputDireccion = document.getElementById(prefix + '-direccion'); const inputDocumento = document.getElementById(prefix + '-documento'); if (!inputNombre || !inputTelefono || !inputDireccion || !inputDocumento) return; if (!id) { inputDocumento.value = ''; return; } const cliente = clientesRegistradosCache.find(c => parseInt(c.id, 10) === id); if (!cliente) return; inputNombre.value = normalizarTexto(cliente.nombre); inputTelefono.value = normalizarTexto(cliente.telefono); inputDireccion.value = normalizarTexto(cliente.direccion); inputDocumento.value = normalizarTexto(cliente.documento); } function poblarSelectClientes(selectEl) { if (!selectEl) return; selectEl.innerHTML = '<option value="">Seleccionar cliente...</option><option value="no_registrado">Cliente no registrado</option>'; clientesRegistradosCache.forEach(c => { const nombre = normalizarTexto(c.nombre) || 'Cliente'; const telefono = normalizarTexto(c.telefono); const documento = normalizarTexto(c.documento); const extra = documento ? ` | Doc: ${documento}` : ''; const telTxt = telefono ? ` | ${telefono}` : ''; const opt = document.createElement('option'); opt.value = c.id; opt.textContent = `${nombre}${telTxt}${extra}`; selectEl.appendChild(opt); }); } function limpiarDatosCliente(prefix) { try { document.getElementById(prefix + '-nombre').value = ''; } catch (_) {} try { document.getElementById(prefix + '-telefono').value = ''; } catch (_) {} try { document.getElementById(prefix + '-direccion').value = ''; } catch (_) {} try { document.getElementById(prefix + '-documento').value = ''; } catch (_) {} } function setEstadoCamposCliente(prefix, visible, enabled, documentoEditable) { const cont = document.getElementById(prefix + '-cliente-fields'); if (!cont) return; cont.style.display = visible ? 'block' : 'none'; const controls = cont.querySelectorAll('input, textarea, select'); controls.forEach(el => { el.disabled = !enabled; }); const doc = document.getElementById(prefix + '-documento'); if (doc) { doc.readOnly = !documentoEditable; } } function obtenerClienteIdParaEnviar(prefix) { const sel = document.getElementById(prefix + '-cliente-select'); if (!sel) return ''; const v = (sel.value || '').trim(); if (!v || v === 'no_registrado') return ''; return v; } function manejarSeleccionCliente(prefix) { const sel = document.getElementById(prefix + '-cliente-select'); if (!sel) return; const v = (sel.value || '').trim(); const esNoRegistrado = v === 'no_registrado'; const id = parseInt(v, 10); const esRegistrado = Number.isFinite(id) && id > 0; if (esNoRegistrado) { limpiarDatosCliente(prefix); setEstadoCamposCliente(prefix, true, true, true); return; } if (esRegistrado) { // Cliente registrado: autocompletar pero no mostrar campos (tal como pediste) rellenarDatosClientePorId(v, prefix); setEstadoCamposCliente(prefix, false, false, false); return; } limpiarDatosCliente(prefix); setEstadoCamposCliente(prefix, false, false, false); } function cargarClientesRegistrados() { return fetch('clientes.php?action=list') .then(r => r.json()) .then(items => { clientesRegistradosCache = Array.isArray(items) ? items : []; poblarSelectClientes(document.getElementById('nueva-venta-cliente-select')); poblarSelectClientes(document.getElementById('edit-compra-cliente-select')); manejarSeleccionCliente('nueva-venta'); manejarSeleccionCliente('edit-compra'); }) .catch(() => { clientesRegistradosCache = []; const nv = document.getElementById('nueva-venta-cliente-select'); const ed = document.getElementById('edit-compra-cliente-select'); if (nv) nv.innerHTML = '<option value="">No se pudieron cargar</option>'; if (ed) ed.innerHTML = '<option value="">No se pudieron cargar</option>'; }); } let logoFacturaDataUrl = null; async function obtenerLogoFactura() { if (logoFacturaDataUrl) return logoFacturaDataUrl; try { const response = await fetch('images/logo.png'); if (!response.ok) { throw new Error('Logo no disponible'); } const blob = await response.blob(); logoFacturaDataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); return logoFacturaDataUrl; } catch (error) { console.warn('No se pudo cargar el logo para la factura:', error); return null; } } function formatearMoneda(valor) { const numero = Number(valor) || 0; const locale = MONEDA_LOCALE || 'es-ES'; try { return MONEDA_SIMBOLO + numero.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } catch (e) { return MONEDA_SIMBOLO + numero.toFixed(2); } } function formatearFechaFactura(fecha) { if (!fecha) return new Date().toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }); const fechaObj = new Date(fecha); if (Number.isNaN(fechaObj.getTime())) { return new Date().toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }); } return fechaObj.toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }); } async function generarFactura(compra) { const { jsPDF } = window.jspdf || {}; if (!jsPDF) { throw new Error('jsPDF no está disponible'); } const doc = new jsPDF({ unit: 'pt', format: 'letter' }); if (typeof doc.autoTable !== 'function') { throw new Error('autoTable no está disponible'); } const pageWidth = doc.internal.pageSize.getWidth(); const margin = 28; const tableWidth = pageWidth - margin * 2; const logo = await obtenerLogoFactura(); if (logo) { try { const logoSize = 50; doc.addImage(logo, 'PNG', margin, margin, logoSize, logoSize); } catch (error) { console.warn('No se pudo agregar el logo a la factura:', error); } } const facturaId = (compra && (compra.venta_id || compra.id)) ? (compra.venta_id || compra.id) : null; const facturaNumero = facturaId ? String(facturaId).padStart(4, '0') : new Date().getTime().toString().slice(-6); const fechaVenta = formatearFechaFactura(compra && compra.fecha); const items = (compra && Array.isArray(compra.items) && compra.items.length > 0) ? compra.items : null; const totalCalc = items ? items.reduce((acc, it) => acc + (Number(it && it.precio) || 0), 0) : (Number(compra && compra.precio) || 0); const montoTotal = formatearMoneda((Number(compra && compra.total) || 0) || totalCalc); const documentoCliente = compra && compra.documento ? String(compra.documento) : ''; const descripcionCompra = compra && compra.descripcion ? String(compra.descripcion) : ''; const tituloY = margin + 30; doc.setFont('helvetica', 'bold'); doc.setFontSize(16); doc.text('Factura de Venta', pageWidth / 2, tituloY, { align: 'center' }); doc.setFontSize(9); doc.setFont('helvetica', 'normal'); const infoInicioY = margin + 76; const vendedorInfo = [ DATOS_VENDEDOR.nombre, `Tel: ${DATOS_VENDEDOR.telefono}`, `Correo: ${DATOS_VENDEDOR.correo}`, DATOS_VENDEDOR.direccion ]; vendedorInfo.forEach((linea, index) => { doc.text(linea, margin, infoInicioY + index * 12); }); const facturaInfo = [ `Factura Nº INV-${facturaNumero}`, `Fecha de emisión: ${new Date().toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}` ]; facturaInfo.forEach((linea, index) => { doc.text(linea, pageWidth - margin, infoInicioY + index * 12, { align: 'right' }); }); const detallesStartY = infoInicioY + Math.max(vendedorInfo.length, facturaInfo.length) * 12 + 14; const detallesVenta = [ ['Cliente', compra && compra.nombre ? compra.nombre : 'No especificado'], ['Documento', documentoCliente || 'No especificado'], ['Teléfono', compra && compra.telefono ? compra.telefono : 'No especificado'], ['Dirección', compra && compra.direccion ? compra.direccion : 'No especificada'], ['Fecha de la Venta', fechaVenta], ['Descripción', descripcionCompra || '-'] ]; doc.autoTable({ head: [['Detalle', 'Información']], body: detallesVenta, startY: detallesStartY, margin: { left: margin, right: margin }, tableWidth, theme: 'grid', tableLineColor: [0, 0, 0], tableLineWidth: 0.4, headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontSize: 9, fontStyle: 'bold' }, styles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontSize: 8, cellPadding: 4, lineColor: [0, 0, 0], lineWidth: 0.4 }, alternateRowStyles: { fillColor: [255, 255, 255] }, columnStyles: { 0: { cellWidth: 150, fontStyle: 'bold' }, 1: { cellWidth: tableWidth - 150 } } }); const itemsBody = (items && items.length > 0) ? items.map(it => { const tipo = (it && it.tipo) ? String(it.tipo).toUpperCase() : '-'; const nombre = (it && it.item_nombre) ? String(it.item_nombre) : '-'; const precio = formatearMoneda(Number(it && it.precio) || 0); return [tipo, nombre, precio]; }) : [[ (compra && compra.tipo) ? String(compra.tipo).toUpperCase() : '-', (compra && compra.item_nombre) ? String(compra.item_nombre) : '-', formatearMoneda(Number(compra && compra.precio) || 0) ]]; const itemsStartY = (doc.lastAutoTable && doc.lastAutoTable.finalY) ? (doc.lastAutoTable.finalY + 10) : (detallesStartY + 90); doc.autoTable({ head: [['Tipo', 'Item', 'Precio']], body: itemsBody, startY: itemsStartY, margin: { left: margin, right: margin }, tableWidth, theme: 'grid', tableLineColor: [0, 0, 0], tableLineWidth: 0.4, headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontSize: 9, fontStyle: 'bold' }, styles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontSize: 8, cellPadding: 4, lineColor: [0, 0, 0], lineWidth: 0.4 }, alternateRowStyles: { fillColor: [255, 255, 255] }, columnStyles: { 0: { cellWidth: 80, fontStyle: 'bold' }, 1: { cellWidth: tableWidth - 80 - 110 }, 2: { cellWidth: 110, halign: 'right' } } }); const resumenStartY = doc.lastAutoTable.finalY + 12; doc.autoTable({ startY: resumenStartY, margin: { left: margin, right: margin }, tableWidth, body: [ [ { content: 'Subtotal', styles: { halign: 'right', fontStyle: 'bold' } }, { content: montoTotal, styles: { halign: 'right' } } ], [ { content: 'Impuestos', styles: { halign: 'right', fontStyle: 'bold' } }, { content: formatearMoneda(0), styles: { halign: 'right' } } ], [ { content: 'Total', styles: { halign: 'right', fontStyle: 'bold', fontSize: 10 } }, { content: montoTotal, styles: { halign: 'right', fontSize: 10 } } ] ], theme: 'grid', tableLineColor: [0, 0, 0], tableLineWidth: 0.4, styles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], cellPadding: 4, fontSize: 9 }, alternateRowStyles: { fillColor: [255, 255, 255] }, columnStyles: { 0: { cellWidth: tableWidth - 140 }, 1: { cellWidth: 140 } } }); const nombreSeguro = (compra && compra.nombre ? compra.nombre : 'cliente').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'cliente'; doc.save(`factura-${facturaNumero}-${nombreSeguro}.pdf`); } function escapeHtml(valor) { if (valor === null || valor === undefined) return ''; return String(valor) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatearHora12(fecha) { const d = (fecha instanceof Date) ? fecha : new Date(fecha); const horas24 = d.getHours(); const minutos = String(d.getMinutes()).padStart(2, '0'); const ampm = horas24 >= 12 ? 'PM' : 'AM'; let horas12 = horas24 % 12; if (horas12 === 0) horas12 = 12; const hh = String(horas12).padStart(2, '0'); return `${hh}:${minutos} ${ampm}`; } function generarHtmlTicket57mm(compra, logoDataUrl) { const facturaId = (compra && (compra.venta_id || compra.id)) ? (compra.venta_id || compra.id) : null; const facturaNumero = facturaId ? String(facturaId).padStart(4, '0') : new Date().getTime().toString().slice(-6); const fechaVenta = formatearFechaFactura(compra && compra.fecha); const items = (compra && Array.isArray(compra.items) && compra.items.length > 0) ? compra.items : null; const totalCalc = items ? items.reduce((acc, it) => acc + (Number(it && it.precio) || 0), 0) : (Number(compra && compra.precio) || 0); const total = (Number(compra && compra.total) || 0) || totalCalc; const clienteNombre = (compra && compra.nombre) ? String(compra.nombre) : 'No especificado'; const clienteDocumento = (compra && compra.documento) ? String(compra.documento) : ''; const clienteTelefono = (compra && compra.telefono) ? String(compra.telefono) : ''; const clienteDireccion = (compra && compra.direccion) ? String(compra.direccion) : ''; const descripcionCompra = (compra && compra.descripcion) ? String(compra.descripcion) : ''; const itemsHtml = (items && items.length > 0) ? items.map((it) => { const nombre = escapeHtml((it && it.item_nombre) ? String(it.item_nombre) : '-'); const tipo = escapeHtml((it && it.tipo) ? String(it.tipo) : ''); const precioNum = Number(it && it.precio) || 0; const precioTxt = escapeHtml(formatearMoneda(precioNum)); return ` <div class="row item"> <div class="col name"> <div class="title">${nombre}</div> <div class="meta">${tipo ? tipo.toUpperCase() : ''}</div> </div> <div class="col price">${precioTxt}</div> </div> `; }).join('') : (() => { const nombre = escapeHtml((compra && compra.item_nombre) ? String(compra.item_nombre) : '-'); const tipo = escapeHtml((compra && compra.tipo) ? String(compra.tipo) : ''); const precioTxt = escapeHtml(formatearMoneda(Number(compra && compra.precio) || 0)); return ` <div class="row item"> <div class="col name"> <div class="title">${nombre}</div> <div class="meta">${tipo ? tipo.toUpperCase() : ''}</div> </div> <div class="col price">${precioTxt}</div> </div> `; })(); const logoHtml = logoDataUrl ? `<div class="logo-wrap"><img id="ticket-logo" class="logo" src="${logoDataUrl}" alt="Logo"></div>` : ''; const vendedorNombre = escapeHtml((DATOS_VENDEDOR && DATOS_VENDEDOR.nombre) ? DATOS_VENDEDOR.nombre : ''); const vendedorTelefono = escapeHtml((DATOS_VENDEDOR && DATOS_VENDEDOR.telefono) ? DATOS_VENDEDOR.telefono : ''); const vendedorCorreo = escapeHtml((DATOS_VENDEDOR && DATOS_VENDEDOR.correo) ? DATOS_VENDEDOR.correo : ''); const vendedorDireccion = escapeHtml((DATOS_VENDEDOR && DATOS_VENDEDOR.direccion) ? DATOS_VENDEDOR.direccion : ''); const now = new Date(); const nowTxt = escapeHtml(`${now.toLocaleDateString('es-ES')} ${formatearHora12(now)}`); const totalTxt = escapeHtml(formatearMoneda(total)); return ` <!doctype html> <html lang="es"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Factura INV-${escapeHtml(facturaNumero)}</title> <style> @page { size: auto; margin: 0mm; } html, body { width: 100%; padding: 0; margin: 0; } body { font-family: Arial, Helvetica, sans-serif; color: #000; background: #fff; font-size: 10px; } * { box-sizing: border-box; } .ticket { width: 100%; padding: 2mm; } .logo-wrap { display: flex; justify-content: center; margin-bottom: 2mm; } .logo { max-width: 80%; max-height: 18mm; object-fit: contain; } .center { text-align: center; } .h1 { font-size: 13px; font-weight: 700; margin: 0 0 1mm 0; } .h2 { font-size: 11px; font-weight: 700; margin: 0 0 1mm 0; } .p { font-size: 10px; margin: 0.2mm 0; line-height: 1.25; } .muted { opacity: 0.85; } .sep { border-top: 1px dashed #000; margin: 2mm 0; } .row { display: flex; justify-content: space-between; gap: 1.5mm; } .col { min-width: 0; } .name { flex: 1; } .price { width: 22%; text-align: right; font-variant-numeric: tabular-nums; } .item { margin: 1.2mm 0; } .item { page-break-inside: avoid; break-inside: avoid; } .title { font-size: 10px; font-weight: 700; white-space: normal; word-break: break-word; } .meta { font-size: 9px; opacity: 0.85; white-space: normal; word-break: break-word; } .kv { display: flex; justify-content: space-between; gap: 2mm; font-size: 10px; margin: 0.8mm 0; } .kv .k { font-weight: 700; } .kv .v { text-align: right; font-variant-numeric: tabular-nums; } .big { font-size: 12px; font-weight: 800; } .foot { margin-top: 2mm; font-size: 10px; } @media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } } @media print and (min-width: 500px) { body { font-size: 14px; } .ticket { padding: 14mm; } .logo { max-height: 30mm; } .h1 { font-size: 24px; } .h2 { font-size: 18px; } .p { font-size: 14px; } .sep { margin: 6mm 0; } .row { gap: 6mm; } .price { width: 25%; } .item { margin: 3mm 0; } .title { font-size: 14px; } .meta { font-size: 12px; } .kv { font-size: 14px; margin: 2mm 0; } .big { font-size: 18px; } .foot { font-size: 13px; } } </style> </head> <body> <div class="ticket"> ${logoHtml} <div class="center"> <div class="h1">${vendedorNombre}</div> ${vendedorTelefono ? `<div class="p muted">Tel: ${vendedorTelefono}</div>` : ''} ${vendedorCorreo ? `<div class="p muted">${vendedorCorreo}</div>` : ''} ${vendedorDireccion ? `<div class="p muted">${vendedorDireccion}</div>` : ''} </div> <div class="sep"></div> <div class="center"> <div class="h2">FACTURA</div> <div class="p">INV-${escapeHtml(facturaNumero)}</div> <div class="p muted">Emitida: ${nowTxt}</div> <div class="p muted">Venta: ${escapeHtml(fechaVenta)}</div> </div> <div class="sep"></div> <div> <div class="p"><b>Cliente:</b> ${escapeHtml(clienteNombre)}</div> ${clienteDocumento ? `<div class="p"><b>Documento:</b> ${escapeHtml(clienteDocumento)}</div>` : ''} ${clienteTelefono ? `<div class="p"><b>Teléfono:</b> ${escapeHtml(clienteTelefono)}</div>` : ''} ${clienteDireccion ? `<div class="p"><b>Dirección:</b> ${escapeHtml(clienteDireccion)}</div>` : ''} ${descripcionCompra ? `<div class="p"><b>Descripción:</b> ${escapeHtml(descripcionCompra)}</div>` : ''} </div> <div class="sep"></div> <div> ${itemsHtml} </div> <div class="sep"></div> <div class="kv"><div class="k">Subtotal</div><div class="v">${totalTxt}</div></div> <div class="kv"><div class="k">Impuestos</div><div class="v">${escapeHtml(formatearMoneda(0))}</div></div> <div class="kv"><div class="k big">TOTAL</div><div class="v big">${totalTxt}</div></div> <div class="sep"></div> <div class="center foot"> <div class="p"><b>¡Gracias por tu compra!</b></div> <div class="p muted">Este comprobante fue generado por el sistema.</div> </div> </div> </body> </html> `; } async function imprimirFactura(compra, ventanaPreAbierta = null) { if (imprimirFactura && imprimirFactura._inProgress) return; imprimirFactura._inProgress = true; try { let logo = null; try { logo = await obtenerLogoFactura(); } catch (_) { logo = null; } const html = generarHtmlTicket57mm(compra, logo); const usarVentana = !!(ventanaPreAbierta && !ventanaPreAbierta.closed); const imprimirHtmlEnIframe = (htmlTicket) => new Promise((resolve, reject) => { const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.left = '-10000px'; iframe.style.top = '0'; iframe.style.width = '800px'; iframe.style.height = '600px'; iframe.style.border = '0'; iframe.style.background = '#fff'; iframe.style.pointerEvents = 'none'; iframe.setAttribute('aria-hidden', 'true'); let blobUrl = null; try { const blob = new Blob([htmlTicket], { type: 'text/html' }); blobUrl = URL.createObjectURL(blob); } catch (e) { blobUrl = null; } const limpiar = () => { try { if (blobUrl) URL.revokeObjectURL(blobUrl); } catch (_) {} try { iframe.remove(); } catch (_) {} }; iframe.onerror = () => { limpiar(); reject(new Error('No se pudo crear el contexto de impresión')); }; iframe.onload = () => { try { iframe.onload = null; const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); const w = iframe.contentWindow; if (!doc || !w) { limpiar(); reject(new Error('Contexto de impresión no disponible')); return; } // Algunos navegadores disparan onload pero el body puede venir vacío con Blob URLs. // Si está vacío, reinyectamos el HTML como respaldo. try { if (!doc.body || (doc.body && doc.body.children && doc.body.children.length === 0)) { doc.open(); doc.write(htmlTicket); doc.close(); } } catch (_) {} try { const texto = (doc.body && doc.body.textContent) ? doc.body.textContent.trim() : ''; if (!texto) { limpiar(); reject(new Error('Contenido de impresión vacío')); return; } } catch (_) {} const cerrarLuego = () => { setTimeout(limpiar, 200); }; try { w.onafterprint = () => { cerrarLuego(); }; } catch (_) {} let didPrint = false; const imprimir = () => { try { if (didPrint) return; didPrint = true; w.focus(); w.print(); resolve(); setTimeout(cerrarLuego, 10000); } catch (e) { limpiar(); reject(e); } }; const img = doc.getElementById('ticket-logo'); if (!img) { w.requestAnimationFrame(() => setTimeout(imprimir, 650)); return; } if (img.complete) { w.requestAnimationFrame(() => setTimeout(imprimir, 650)); return; } const done = () => w.requestAnimationFrame(() => setTimeout(imprimir, 650)); img.onload = done; img.onerror = done; } catch (e) { limpiar(); reject(e); } }; document.body.appendChild(iframe); if (blobUrl) { iframe.src = blobUrl; } else { // Fallback: si no se puede crear Blob URL, usar about:blank y escribir el HTML. iframe.src = 'about:blank'; iframe.onload = () => { try { iframe.onload = null; const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); if (!doc) throw new Error('Documento no disponible'); doc.open(); doc.write(htmlTicket); doc.close(); setTimeout(() => { try { const w = iframe.contentWindow; if (!w) throw new Error('Ventana no disponible'); w.focus(); w.print(); resolve(); } catch (e) { reject(e); } }, 200); } catch (e) { limpiar(); reject(e); } }; } }); if (!usarVentana) { try { await imprimirHtmlEnIframe(html); return; } catch (e) { console.warn('Fallo impresión por iframe, intentando ventana nueva:', e); } } const w = usarVentana ? ventanaPreAbierta : window.open('about:blank', '_blank', 'width=420,height=720'); if (!w) { throw new Error('No se pudo abrir la ventana de impresión'); } try { w.opener = null; } catch (_) {} w.document.open(); w.document.write(html); w.document.close(); let didPrint = false; const imprimir = () => { if (didPrint) return; didPrint = true; w.focus(); w.print(); }; const cerrarLuego = () => { try { w.close(); } catch (_) {} }; try { w.onafterprint = () => { setTimeout(cerrarLuego, 200); }; } catch (_) {} const imprimirCuandoListo = () => { try { const bodyText = (w.document.body && w.document.body.textContent) ? w.document.body.textContent.trim() : ''; if (!bodyText) { setTimeout(imprimirCuandoListo, 120); return; } const img = w.document.getElementById('ticket-logo'); if (!img) { setTimeout(imprimir, 120); return; } if (img.complete) { setTimeout(imprimir, 120); return; } const done = () => setTimeout(imprimir, 120); img.onload = done; img.onerror = done; } catch (_) { setTimeout(imprimirCuandoListo, 120); } }; setTimeout(imprimirCuandoListo, 150); } finally { imprimirFactura._inProgress = false; } } // Editar compra function editarCompra(id) { const compraId = parseInt(id || 0, 10); if (!compraId) return; const pClientes = (typeof cargarClientesRegistrados === 'function') ? cargarClientesRegistrados() : Promise.resolve(); const pCompra = fetch('proyectos.php?action=get_compra&id=' + encodeURIComponent(compraId)) .then(res => res.json()); Promise.all([pClientes, pCompra]) .then(([, compra]) => { if (!compra || compra.success === false) { mostrarMensaje((compra && compra.error) ? compra.error : 'No se pudo cargar la compra.', false); return; } const itemBase = (compra && Array.isArray(compra.items) && compra.items.length > 0) ? (compra.items[0] || {}) : compra; const idEdicion = (itemBase && itemBase.id) ? itemBase.id : compraId; editVentaItemEditando = -1; editVentaItems = (compra && Array.isArray(compra.items) && compra.items.length > 0) ? compra.items.map(it => ({ id: it && it.id ? it.id : null, tipo: it && it.tipo ? it.tipo : '', item_id: it && it.item_id ? it.item_id : '', precio: it && it.precio ? it.precio : '', item_texto: it && it.item_nombre ? it.item_nombre : '' })) : [{ id: compra && compra.id ? compra.id : null, tipo: compra && compra.tipo ? compra.tipo : '', item_id: compra && compra.item_id ? compra.item_id : '', precio: compra && compra.precio ? compra.precio : '', item_texto: compra && compra.item_nombre ? compra.item_nombre : '' }]; try { renderEditVentaItems(); } catch (e) {} document.getElementById('edit-compra-id').value = idEdicion; document.getElementById('edit-compra-nombre').value = compra.nombre || ''; document.getElementById('edit-compra-telefono').value = compra.telefono || ''; document.getElementById('edit-compra-direccion').value = compra.direccion || ''; document.getElementById('edit-compra-documento').value = compra.documento || ''; document.getElementById('edit-compra-descripcion').value = compra.descripcion || ''; document.getElementById('edit-compra-tipo').value = itemBase.tipo || ''; document.getElementById('edit-compra-precio').value = itemBase.precio || ''; document.getElementById('edit-compra-item-id').value = itemBase.item_id || ''; const selectCliente = document.getElementById('edit-compra-cliente-select'); if (selectCliente) { if (compra.cliente_id) { selectCliente.value = compra.cliente_id; } else { selectCliente.value = 'no_registrado'; } } // Aplicar reglas de visibilidad: solo mostrar campos si es no registrado. // Caso especial: si la compra no tiene cliente_id, mostramos campos con los datos existentes (sin limpiar). if (selectCliente && selectCliente.value === 'no_registrado') { setEstadoCamposCliente('edit-compra', true, true, true); } else { manejarSeleccionCliente('edit-compra'); } // Si es cliente registrado, refrescar valores desde el registro (incluye documento) if (selectCliente && selectCliente.value && selectCliente.value !== 'no_registrado') { rellenarDatosClientePorId(selectCliente.value, 'edit-compra'); } cargarItemsPorTipo(itemBase.tipo || '', itemBase.item_id || null); document.getElementById('edit-compra-modal').style.display = 'flex'; }) .catch(() => { mostrarMensaje('No se pudo cargar la compra.', false); }); } // Cargar items según el tipo seleccionado function cargarItemsPorTipo(tipo, itemIdSeleccionado = null) { const selectItem = document.getElementById('edit-compra-item'); selectItem.innerHTML = '<option value="">Cargando...</option>'; if (tipo === 'proyecto') { fetch('proyectos.php?action=list') .then(res => res.json()) .then(proyectos => { selectItem.innerHTML = '<option value="">Seleccionar proyecto...</option>'; proyectos.forEach(proyecto => { const selected = proyecto.id == itemIdSeleccionado ? 'selected' : ''; const precioFormateado = formatearMoneda(proyecto.precio); selectItem.innerHTML += `<option value="${proyecto.id}" ${selected}>${proyecto.nombre} - ${precioFormateado}</option>`; }); }); } else if (tipo === 'servicio') { fetch('servicios.php?action=list') .then(res => res.json()) .then(servicios => { selectItem.innerHTML = '<option value="">Seleccionar servicio...</option>'; servicios.forEach(servicio => { const selected = servicio.id == itemIdSeleccionado ? 'selected' : ''; const precioFormateado = formatearMoneda(servicio.precio); selectItem.innerHTML += `<option value="${servicio.id}" ${selected}>${servicio.nombre} - ${precioFormateado}</option>`; }); }); } } // Actualizar precio cuando cambie el item function actualizarPrecioItem() { const tipo = document.getElementById('edit-compra-tipo').value; const itemId = document.getElementById('edit-compra-item').value; if (!tipo || !itemId) return; if (tipo === 'proyecto') { fetch('proyectos.php?action=get&id=' + itemId) .then(res => res.json()) .then(proyecto => { if (proyecto.precio) { document.getElementById('edit-compra-precio').value = proyecto.precio; } }); } else if (tipo === 'servicio') { fetch('servicios.php?action=get&id=' + itemId) .then(res => res.json()) .then(servicio => { if (servicio.precio) { document.getElementById('edit-compra-precio').value = servicio.precio; } }); } } // Funciones para nueva venta let nuevaVentaPrecioEditadoManualmente = false; let nuevaVentaItems = []; let editVentaItems = []; let editVentaItemEditando = -1; function actualizarBotonEditVenta() { const btn = document.getElementById('edit-compra-agregar-item'); if (!btn) return; if (editVentaItemEditando >= 0) { btn.innerHTML = '<i class="fas fa-save"></i> Actualizar item'; } else { btn.innerHTML = '<i class="fas fa-plus"></i> Agregar a la venta'; } } function renderEditVentaItems() { const cont = document.getElementById('edit-compra-items-container'); if (!cont) return; if (!Array.isArray(editVentaItems) || editVentaItems.length === 0) { cont.innerHTML = '<div class="compras-empty">Aún no has agregado items a la venta.</div>'; actualizarBotonEditVenta(); return; } let total = 0; const rows = editVentaItems.map((it, idx) => { const precio = Number(it.precio) || 0; total += precio; const tipoTxt = (it.tipo === 'proyecto') ? 'Proyecto' : 'Servicio'; const nombre = (it.item_texto || it.item_nombre || '').toString(); return ` <tr> <td>${tipoTxt}</td> <td>${nombre}</td> <td style="text-align:right;">${formatearMoneda(precio)}</td> <td style="text-align:right;"> <button type="button" class="action-btn edit-btn" data-ev-edit="${idx}" title="Editar"> <i class="fas fa-edit"></i> </button> <button type="button" class="action-btn delete-btn" data-ev-remove="${idx}" title="Quitar"> <i class="fas fa-trash"></i> </button> </td> </tr> `; }).join(''); cont.innerHTML = ` <div style="overflow:auto;"> <table class="table" style="width:100%; border-collapse:collapse;"> <thead> <tr> <th>Tipo</th> <th>Item</th> <th style="text-align:right;">Precio</th> <th style="text-align:right;">Acción</th> </tr> </thead> <tbody> ${rows} </tbody> </table> </div> <div style="margin-top:10px; text-align:right; font-weight:700;"> Total: ${formatearMoneda(total)} </div> `; cont.querySelectorAll('[data-ev-remove]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.getAttribute('data-ev-remove') || '-1', 10); if (!Number.isFinite(idx) || idx < 0) return; if (Array.isArray(editVentaItems) && editVentaItems.length <= 1) { mostrarMensaje('La venta debe tener al menos un item.', false); return; } editVentaItems.splice(idx, 1); if (editVentaItemEditando === idx) editVentaItemEditando = -1; if (editVentaItemEditando > idx) editVentaItemEditando -= 1; renderEditVentaItems(); }); }); cont.querySelectorAll('[data-ev-edit]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.getAttribute('data-ev-edit') || '-1', 10); if (!Number.isFinite(idx) || idx < 0) return; const it = editVentaItems[idx]; if (!it) return; editVentaItemEditando = idx; const tipoSel = document.getElementById('edit-compra-tipo'); const itemSel = document.getElementById('edit-compra-item'); const precioInp = document.getElementById('edit-compra-precio'); if (tipoSel) tipoSel.value = it.tipo || ''; if (precioInp) precioInp.value = it.precio || ''; try { cargarItemsPorTipo(it.tipo || '', it.item_id || null); } catch (e) {} setTimeout(() => { try { if (itemSel) itemSel.value = it.item_id || ''; } catch (e) {} }, 200); actualizarBotonEditVenta(); }); }); actualizarBotonEditVenta(); } function intentarAgregarItemEditVenta() { const tipo = (document.getElementById('edit-compra-tipo')?.value || '').trim(); const itemSelect = document.getElementById('edit-compra-item'); const itemId = (itemSelect?.value || '').trim(); const precioStr = (document.getElementById('edit-compra-precio')?.value || '').trim(); const precio = Number(precioStr); if (!tipo) { mostrarMensaje('Selecciona el tipo del item.', false); return false; } if (!itemId) { mostrarMensaje('Selecciona un item.', false); return false; } if (!Number.isFinite(precio) || precio <= 0) { mostrarMensaje('Ingresa un precio válido.', false); return false; } const option = itemSelect && itemSelect.selectedOptions && itemSelect.selectedOptions[0] ? itemSelect.selectedOptions[0] : null; const itemTexto = option ? option.textContent : ''; if (!Array.isArray(editVentaItems)) editVentaItems = []; if (Number.isFinite(editVentaItemEditando) && editVentaItemEditando >= 0 && editVentaItems[editVentaItemEditando]) { const prev = editVentaItems[editVentaItemEditando]; editVentaItems[editVentaItemEditando] = { id: prev && prev.id ? prev.id : null, tipo, item_id: itemId, precio: precio, item_texto: itemTexto }; editVentaItemEditando = -1; renderEditVentaItems(); return true; } editVentaItems.push({ tipo, item_id: itemId, precio: precio, item_texto: itemTexto }); renderEditVentaItems(); return true; } function renderNuevaVentaItems() { const cont = document.getElementById('nueva-venta-items-container'); if (!cont) return; if (!Array.isArray(nuevaVentaItems) || nuevaVentaItems.length === 0) { cont.innerHTML = '<div class="compras-empty">Aún no has agregado items a la venta.</div>'; return; } let total = 0; const rows = nuevaVentaItems.map((it, idx) => { const precio = Number(it.precio) || 0; total += precio; const tipoTxt = (it.tipo === 'proyecto') ? 'Proyecto' : 'Servicio'; const nombre = it.item_texto || ''; return ` <tr> <td>${tipoTxt}</td> <td>${nombre}</td> <td style="text-align:right;">${formatearMoneda(precio)}</td> <td style="text-align:right;"> <button type="button" class="action-btn delete-btn" data-nv-remove="${idx}" title="Quitar"> <i class="fas fa-trash"></i> </button> </td> </tr> `; }).join(''); cont.innerHTML = ` <div style="overflow:auto;"> <table class="table" style="width:100%; border-collapse:collapse;"> <thead> <tr> <th>Tipo</th> <th>Item</th> <th style="text-align:right;">Precio</th> <th style="text-align:right;">Acción</th> </tr> </thead> <tbody> ${rows} </tbody> </table> </div> <div style="margin-top:10px; text-align:right; font-weight:700;"> Total: ${formatearMoneda(total)} </div> `; cont.querySelectorAll('[data-nv-remove]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.getAttribute('data-nv-remove') || '-1', 10); if (Number.isFinite(idx) && idx >= 0) { nuevaVentaItems.splice(idx, 1); renderNuevaVentaItems(); } }); }); } function intentarAgregarItemNuevaVenta() { const tipo = (document.getElementById('nueva-venta-tipo')?.value || '').trim(); const itemSelect = document.getElementById('nueva-venta-item'); const itemId = (itemSelect?.value || '').trim(); const precioStr = (document.getElementById('nueva-venta-precio')?.value || '').trim(); const precio = Number(precioStr); if (!tipo) { mostrarMensaje('Selecciona el tipo del item.', false); return false; } if (!itemId) { mostrarMensaje('Selecciona un item.', false); return false; } if (!Number.isFinite(precio) || precio <= 0) { mostrarMensaje('Ingresa un precio válido.', false); return false; } const option = itemSelect && itemSelect.selectedOptions && itemSelect.selectedOptions[0] ? itemSelect.selectedOptions[0] : null; const itemTexto = option ? option.textContent : ''; nuevaVentaItems.push({ tipo, item_id: itemId, precio: precio, item_texto: itemTexto }); try { document.getElementById('nueva-venta-tipo').value = ''; } catch (e) {} try { document.getElementById('nueva-venta-item').innerHTML = '<option value="">Primero selecciona el tipo...</option>'; } catch (e) {} try { document.getElementById('nueva-venta-precio').value = ''; } catch (e) {} nuevaVentaPrecioEditadoManualmente = false; renderNuevaVentaItems(); return true; } function abrirModalNuevaVenta() { // Establecer fecha actual por defecto const ahora = new Date(); const fechaActual = ahora.toISOString().slice(0, 10); document.getElementById('nueva-venta-fecha').value = fechaActual; document.getElementById('nueva-venta-precio').value = ''; nuevaVentaPrecioEditadoManualmente = false; nuevaVentaItems = []; try { renderNuevaVentaItems(); } catch (e) {} try { const selectCliente = document.getElementById('nueva-venta-cliente-select'); if (selectCliente) selectCliente.value = ''; limpiarDatosCliente('nueva-venta'); const descInput = document.getElementById('nueva-venta-descripcion'); if (descInput) descInput.value = ''; setEstadoCamposCliente('nueva-venta', false, false, false); } catch (e) {} try { cargarClientesRegistrados(); } catch (e) {} // Mostrar modal document.getElementById('nueva-venta-modal').style.display = 'flex'; } function cargarItemsNuevaVenta(tipo) { const selectItem = document.getElementById('nueva-venta-item'); selectItem.innerHTML = '<option value="">Cargando...</option>'; if (tipo === 'proyecto') { fetch('proyectos.php?action=list') .then(res => res.json()) .then(proyectos => { selectItem.innerHTML = '<option value="">Seleccionar proyecto...</option>'; proyectos.forEach(proyecto => { const precioFormateado = formatearMoneda(proyecto.precio); selectItem.innerHTML += `<option value="${proyecto.id}">${proyecto.nombre} - ${precioFormateado}</option>`; }); }); } else if (tipo === 'servicio') { fetch('servicios.php?action=list') .then(res => res.json()) .then(servicios => { selectItem.innerHTML = '<option value="">Seleccionar servicio...</option>'; servicios.forEach(servicio => { const precioFormateado = formatearMoneda(servicio.precio); selectItem.innerHTML += `<option value="${servicio.id}">${servicio.nombre} - ${precioFormateado}</option>`; }); }); } else { selectItem.innerHTML = '<option value="">Primero selecciona el tipo...</option>'; } } function actualizarPrecioNuevaVenta() { const tipo = document.getElementById('nueva-venta-tipo').value; const itemId = document.getElementById('nueva-venta-item').value; if (!tipo || !itemId || nuevaVentaPrecioEditadoManualmente) return; if (tipo === 'proyecto') { fetch('proyectos.php?action=get&id=' + itemId) .then(res => res.json()) .then(proyecto => { if (proyecto.precio && !nuevaVentaPrecioEditadoManualmente) { document.getElementById('nueva-venta-precio').value = proyecto.precio; } }); } else if (tipo === 'servicio') { fetch('servicios.php?action=get&id=' + itemId) .then(res => res.json()) .then(servicio => { if (servicio.precio && !nuevaVentaPrecioEditadoManualmente) { document.getElementById('nueva-venta-precio').value = servicio.precio; } }); } } // Eliminar compra function eliminarCompra(id) { if (!confirm('¿Seguro que deseas eliminar esta compra?')) return; fetch('proyectos.php?action=delete_compra', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'id=' + encodeURIComponent(id) }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Compra eliminada.', true); cargarCompras(); } else { mostrarMensaje(data.error || 'Error al eliminar la compra.', false); } }); } // Mensaje bonito function mostrarMensaje(msg, exito) { let div = document.getElementById('mensaje-servicio'); if (!div) { div = document.createElement('div'); div.id = 'mensaje-servicio'; div.style.position = 'fixed'; div.style.top = '30px'; div.style.right = '30px'; div.style.zIndex = 9999; div.style.padding = '18px 32px'; div.style.borderRadius = '14px'; div.style.fontWeight = '700'; div.style.fontSize = '1.1rem'; div.style.boxShadow = '0 4px 18px rgba(44,62,80,0.13)'; div.style.marginBottom = '20px'; // Espacio extra document.body.appendChild(div); } div.innerHTML = (exito ? '✅ ' : '❌ ') + msg; div.style.background = exito ? 'linear-gradient(90deg,#25d366,#128c7e)' : 'linear-gradient(90deg,#e74c3c,#c0392b)'; div.style.color = '#fff'; div.style.display = 'block'; setTimeout(() => { div.style.display = 'none'; }, 2500); } // Inicializar compras al cargar la sección if (document.getElementById('compras-section')) { cargarCompras(); } // --- Navegación de secciones (sidebar) --- document.addEventListener('DOMContentLoaded', function() { const menuItems = document.querySelectorAll('.admin-menu .menu-item'); const sections = document.querySelectorAll('.admin-content .section'); function showSection(key) { let targetId = key + '-section'; let targetSection = document.getElementById(targetId); if (!targetSection) { key = 'dashboard'; targetId = 'dashboard-section'; targetSection = document.getElementById(targetId); } sections.forEach(sec => { sec.classList.toggle('active', sec === targetSection); }); menuItems.forEach(mi => mi.classList.toggle('active', mi.getAttribute('data-section') === key)); try { localStorage.setItem('adminActiveSection', key); } catch (e) {} // Cargas perezosas por sección try { if (key === 'services') cargarServicios(); else if (key === 'projects') cargarProyectos(); else if (key === 'users') cargarUsuarios(); else if (key === 'compras') cargarCompras(); else if (key === 'clientes') cargarClientesSatisfechos(); } catch (e) {} } menuItems.forEach(mi => { mi.addEventListener('click', () => showSection(mi.getAttribute('data-section'))); }); let initialSection = 'dashboard'; try { const stored = localStorage.getItem('adminActiveSection'); if (stored) initialSection = stored; } catch (e) {} showSection(initialSection); // Actualizar métricas del dashboard al cargar try { actualizarServiciosActivos(); } catch (e) {} try { actualizarProyectosCompletados(); } catch (e) {} try { actualizarUsuariosTotales(); } catch (e) {} try { actualizarClientesSatisfechosTotales(); } catch (e) {} }); // Función para generar colores aleatorios para el avatar function generarColorAvatar(inicial) { const colores = [ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Púrpura 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', // Rosa 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Azul claro 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // Verde 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // Rosa amarillo 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', // Turquesa rosa 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', // Rosa claro 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', // Naranja 'linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%)', // Melocotón 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', // Lavanda 'linear-gradient(135deg, #fad0c4 0%, #ffd1ff 100%)', // Rosa suave 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', // Durazno 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', // Rosa 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', // Magenta 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Cian 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // Verde agua 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // Amarillo rosa 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', // Turquesa 'linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%)', // Coral 'linear-gradient(135deg, #ffecd2 0%, #ffd1ff 100%)', // Melocotón claro 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', // Violeta 'linear-gradient(135deg, #fad0c4 0%, #fcb69f 100%)', // Salmón 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', // Rosa fuerte 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Azul cielo 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' // Verde menta ]; // Usar el código ASCII de la inicial para seleccionar un color const codigo = inicial.charCodeAt(0); const indice = codigo % colores.length; return colores[indice]; } // Aplicar color aleatorio al avatar cuando se carga la página document.addEventListener('DOMContentLoaded', function() { const avatar = document.querySelector('.user-avatar'); if (avatar) { const inicial = avatar.textContent.trim(); const colorGradiente = generarColorAvatar(inicial); avatar.style.background = colorGradiente; // Actualizar el color de la sombra para que coincida const colorBase = colorGradiente.includes('#667eea') ? '#667eea' : colorGradiente.includes('#f093fb') ? '#f093fb' : colorGradiente.includes('#4facfe') ? '#4facfe' : colorGradiente.includes('#43e97b') ? '#43e97b' : colorGradiente.includes('#fa709a') ? '#fa709a' : colorGradiente.includes('#a8edea') ? '#a8edea' : colorGradiente.includes('#ff9a9e') ? '#ff9a9e' : colorGradiente.includes('#ffecd2') ? '#ffecd2' : colorGradiente.includes('#a18cd1') ? '#a18cd1' : colorGradiente.includes('#fad0c4') ? '#fad0c4' : '#667eea'; avatar.style.setProperty('--avatar-color', colorBase); } }); // Event listeners para el modal de editar compra document.addEventListener('DOMContentLoaded', function() { try { cargarClientesRegistrados(); } catch (e) {} // Cerrar modal de editar compra document.getElementById('cerrar-edit-compra').addEventListener('click', function() { document.getElementById('edit-compra-modal').style.display = 'none'; }); document.getElementById('cancel-edit-compra').addEventListener('click', function() { document.getElementById('edit-compra-modal').style.display = 'none'; }); // Cerrar modal al hacer clic fuera document.getElementById('edit-compra-modal').addEventListener('click', function(e) { if (e.target === this) { this.style.display = 'none'; } }); // Cambiar tipo de compra document.getElementById('edit-compra-tipo').addEventListener('change', function() { cargarItemsPorTipo(this.value); document.getElementById('edit-compra-precio').value = ''; }); // Cambiar item y actualizar precio document.getElementById('edit-compra-item').addEventListener('change', function() { actualizarPrecioItem(); }); const btnEditAgregarItem = document.getElementById('edit-compra-agregar-item'); if (btnEditAgregarItem) { btnEditAgregarItem.addEventListener('click', function() { intentarAgregarItemEditVenta(); }); } const editClienteSelect = document.getElementById('edit-compra-cliente-select'); if (editClienteSelect) { editClienteSelect.addEventListener('change', function() { manejarSeleccionCliente('edit-compra'); }); } // Enviar formulario de editar compra document.getElementById('edit-compra-form').addEventListener('submit', function(e) { e.preventDefault(); const selectValor = document.getElementById('edit-compra-cliente-select')?.value || ''; if (!selectValor) { mostrarMensaje('Selecciona un cliente registrado o "Cliente no registrado".', false); return; } const formData = new FormData(); formData.append('id', document.getElementById('edit-compra-id').value); formData.append('cliente_id', obtenerClienteIdParaEnviar('edit-compra')); formData.append('nombre', document.getElementById('edit-compra-nombre').value); formData.append('telefono', document.getElementById('edit-compra-telefono').value); formData.append('direccion', document.getElementById('edit-compra-direccion').value); formData.append('descripcion', document.getElementById('edit-compra-descripcion')?.value || ''); formData.append('documento', document.getElementById('edit-compra-documento')?.value || ''); const itemsPayload = (Array.isArray(editVentaItems) && editVentaItems.length > 0) ? editVentaItems.map(it => { const obj = { tipo: it && it.tipo ? it.tipo : '', item_id: it && it.item_id ? it.item_id : '', precio: it && it.precio ? it.precio : '' }; if (it && it.id) obj.id = it.id; return obj; }) : []; if (!itemsPayload.length) { mostrarMensaje('Agrega al menos un proyecto o servicio a la venta.', false); return; } formData.append('items_json', JSON.stringify(itemsPayload)); const first = itemsPayload[0]; formData.append('tipo', first.tipo); formData.append('item_id', first.item_id); formData.append('precio', first.precio); fetch('proyectos.php?action=edit_compra', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Compra actualizada exitosamente.', true); document.getElementById('edit-compra-modal').style.display = 'none'; cargarCompras(); } else { mostrarMensaje(data.error || 'Error al actualizar la compra.', false); } }) .catch(error => { mostrarMensaje('Error de conexión.', false); }); }); // Event listeners para nueva venta // Botón nueva venta document.getElementById('btn-nueva-venta').addEventListener('click', function() { abrirModalNuevaVenta(); }); // Cerrar modal nueva venta document.getElementById('cerrar-nueva-venta').addEventListener('click', function() { document.getElementById('nueva-venta-modal').style.display = 'none'; }); document.getElementById('cancelar-nueva-venta').addEventListener('click', function() { document.getElementById('nueva-venta-modal').style.display = 'none'; }); // Cerrar modal al hacer clic fuera document.getElementById('nueva-venta-modal').addEventListener('click', function(e) { if (e.target === this) { this.style.display = 'none'; } }); // Cambiar tipo de nueva venta document.getElementById('nueva-venta-tipo').addEventListener('change', function() { cargarItemsNuevaVenta(this.value); document.getElementById('nueva-venta-precio').value = ''; nuevaVentaPrecioEditadoManualmente = false; }); // Cambiar item y actualizar precio en nueva venta document.getElementById('nueva-venta-item').addEventListener('change', function() { actualizarPrecioNuevaVenta(); }); const nuevaVentaClienteSelect = document.getElementById('nueva-venta-cliente-select'); if (nuevaVentaClienteSelect) { nuevaVentaClienteSelect.addEventListener('change', function() { manejarSeleccionCliente('nueva-venta'); }); } const nuevaVentaPrecioInput = document.getElementById('nueva-venta-precio'); if (nuevaVentaPrecioInput) { nuevaVentaPrecioInput.addEventListener('input', function() { nuevaVentaPrecioEditadoManualmente = this.value.trim() !== ''; }); } const btnAgregarItemNuevaVenta = document.getElementById('nueva-venta-agregar-item'); if (btnAgregarItemNuevaVenta) { btnAgregarItemNuevaVenta.addEventListener('click', function() { intentarAgregarItemNuevaVenta(); }); } // Enviar formulario de nueva venta document.getElementById('nueva-venta-form').addEventListener('submit', function(e) { e.preventDefault(); const selectValor = document.getElementById('nueva-venta-cliente-select')?.value || ''; if (!selectValor) { mostrarMensaje('Selecciona un cliente registrado o "Cliente no registrado".', false); return; } const fechaVenta = document.getElementById('nueva-venta-fecha').value; if (!fechaVenta) { mostrarMensaje('Selecciona la fecha de la venta.', false); return; } const tipoActual = (document.getElementById('nueva-venta-tipo')?.value || '').trim(); const itemActual = (document.getElementById('nueva-venta-item')?.value || '').trim(); const precioActual = (document.getElementById('nueva-venta-precio')?.value || '').trim(); const hayCamposItemActual = !!(tipoActual || itemActual || precioActual); if (hayCamposItemActual) { const ok = intentarAgregarItemNuevaVenta(); if (!ok) return; } if (!Array.isArray(nuevaVentaItems) || nuevaVentaItems.length === 0) { mostrarMensaje('Agrega al menos un proyecto o servicio a la venta.', false); return; } const formData = new FormData(); formData.append('cliente_id', obtenerClienteIdParaEnviar('nueva-venta')); formData.append('nombre', document.getElementById('nueva-venta-nombre').value); formData.append('telefono', document.getElementById('nueva-venta-telefono').value); formData.append('direccion', document.getElementById('nueva-venta-direccion').value); formData.append('descripcion', document.getElementById('nueva-venta-descripcion')?.value || ''); formData.append('documento', document.getElementById('nueva-venta-documento')?.value || ''); formData.append('items_json', JSON.stringify(nuevaVentaItems)); formData.append('fecha', fechaVenta); fetch('proyectos.php?action=nueva_venta', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { mostrarMensaje('Venta registrada exitosamente.', true); document.getElementById('nueva-venta-modal').style.display = 'none'; document.getElementById('nueva-venta-form').reset(); try { document.getElementById('nueva-venta-documento').value = ''; } catch (e) {} nuevaVentaPrecioEditadoManualmente = false; nuevaVentaItems = []; try { renderNuevaVentaItems(); } catch (e) {} cargarCompras(); } else { mostrarMensaje(data.error || 'Error al registrar la venta.', false); } }) .catch(error => { mostrarMensaje('Error de conexión.', false); }); }); // Inicializar fechas del reporte por defecto try { const inputDesde = document.getElementById('reporte-desde'); const inputHasta = document.getElementById('reporte-hasta'); if (inputDesde && inputHasta) { const hoy = new Date(); const primerDiaMes = new Date(hoy.getFullYear(), hoy.getMonth(), 1); inputDesde.value = primerDiaMes.toISOString().slice(0,10); inputHasta.value = hoy.toISOString().slice(0,10); } } catch (_) {} // Botón descargar reporte const btnReporte = document.getElementById('btn-descargar-reporte'); if (btnReporte) { btnReporte.addEventListener('click', function() { descargarReporteVentas(); }); } }); // Mostrar sección de compras desde el menú // --- REPORTE DE VENTAS PDF --- function formatearFechaYMD(fechaStr) { // Acepta 'YYYY-MM-DD' o timestamp ISO y retorna 'YYYY-MM-DD' if (!fechaStr) return ''; const d = new Date(fechaStr); if (isNaN(d.getTime())) { // intentar si ya es yyyy-mm-dd const m = /^\d{4}-\d{2}-\d{2}/.exec(fechaStr); return m ? m[0] : ''; } return d.toISOString().slice(0,10); } function dentroDeRango(fecha, desde, hasta) { const f = formatearFechaYMD(fecha); if (!f) return false; if (desde && f < desde) return false; if (hasta && f > hasta) return false; return true; } function descargarReporteVentas() { const desde = document.getElementById('reporte-desde')?.value || ''; const hasta = document.getElementById('reporte-hasta')?.value || ''; fetch('proyectos.php?action=compras') .then(res => res.json()) .then(data => { const ventas = Array.isArray(data) ? data : []; const filtradas = ventas.filter(v => dentroDeRango(v.fecha, desde, hasta)); // Preparar datos para la tabla let total = 0; const body = filtradas.map(v => { const items = Array.isArray(v.items) ? v.items : []; const itemsTxt = items.length ? items.map(it => { const t = (it && it.tipo) ? String(it.tipo).toUpperCase() : ''; const n = (it && it.item_nombre) ? String(it.item_nombre) : ''; return t && n ? `${t}: ${n}` : (n || '-'); }).join(' | ') : ((v.item_nombre || '-') + ((v.tipo && v.tipo !== '-') ? ` (${String(v.tipo).toUpperCase()})` : '')); const monto = parseFloat((v.total !== undefined ? v.total : v.precio) || 0); total += monto; const precioFormateado = formatearMoneda(monto); return [ v.nombre || '-', v.telefono || '-', v.direccion || '-', itemsTxt, precioFormateado, formatearFechaYMD(v.fecha) ]; }); const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' }); // Encabezado elegante const title = 'Reporte de Ventas'; const empresa = 'Contador Francisco'; const rango = (desde || hasta) ? `Rango: ${desde || 'inicio'} a ${hasta || 'hoy'}` : 'Rango: todas las fechas'; const fechaGen = new Date(); const generado = `Generado: ${fechaGen.toLocaleDateString('es-VE')} ${fechaGen.toLocaleTimeString('es-VE')}`; doc.setFillColor(44, 62, 80); doc.rect(0, 0, doc.internal.pageSize.getWidth(), 70, 'F'); doc.setTextColor(255,255,255); doc.setFontSize(18); doc.text(title, 40, 30); doc.setFontSize(12); doc.text(empresa, 40, 50); doc.setTextColor(80,80,80); doc.text(rango, 40, 90); doc.text(generado, 40, 110); // Tabla const head = [['Cliente', 'Teléfono', 'Dirección', 'Items', 'Total', 'Fecha']]; doc.autoTable({ head, body, startY: 130, styles: { fontSize: 10 }, headStyles: { fillColor: [39, 174, 96] }, didDrawPage: (dataArg) => { // Pie de página con número de página const str = `Página ${doc.internal.getNumberOfPages()}`; doc.setFontSize(10); doc.setTextColor(120); doc.text(str, doc.internal.pageSize.getWidth() - 60, doc.internal.pageSize.getHeight() - 20); } }); // Total const posY = doc.lastAutoTable && doc.lastAutoTable.finalY ? doc.lastAutoTable.finalY + 20 : 150; doc.setFontSize(14); doc.setTextColor(39, 174, 96); doc.text(`TOTAL: ${formatearMoneda(total)}`, 40, posY); // Guardar const nombreArchivo = `reporte_ventas_${desde || 'inicio'}_${hasta || 'hoy'}.pdf`; doc.save(nombreArchivo); }) .catch(() => { mostrarMensaje('No se pudo generar el reporte.', false); }); } </script> </body> </html>
Coded With 💗 by
0x6ick