Tul xxx Tul
User / IP
:
216.73.216.217
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
emprendo.com.co
/
public_html
/
ecomercial
/
admin
/
Viewing: layout.php
<!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="mobile-web-app-capable" content="yes"> <meta name="theme-color" content="#1a96d3"> <?php // Calculate base path for assets $scriptName = $_SERVER['SCRIPT_NAME'] ?? '/admin/index.php'; $basePath = dirname(dirname($scriptName)); if ($basePath === '/' || $basePath === '\\') { $basePath = ''; } $assetBase = $basePath . '/assets/img/'; ?> <link rel="icon" href="<?= e($assetBase) ?>Favicon-s.png" type="image/png"> <link rel="apple-touch-icon" href="<?= e($assetBase) ?>eComercial.png"> <title><?= e($pageTitle) ?> - Admin eComercial</title> <!-- CSS Resources --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;600;800&display=swap" rel="stylesheet"> <!-- Inline styles --> <style> body{font-family:Jost,sans-serif;background:#f7fafc;color:#12384e} .admin-nav{background:#fff;border-bottom:1px solid #dceaf1;position:sticky;top:0;z-index:10} .brand img{height:60px;width:auto}.brand strong{color:#1a96d3;font-size:1.35rem} .panel{background:#fff;border:1px solid #dceaf1;border-radius:8px;box-shadow:0 10px 24px rgba(18,56,78,.06);padding:22px} .panel > .d-flex:first-child{position:sticky;top:80px;z-index:9;background:#fff;margin:-22px -22px 0;padding:22px 22px 16px;border-bottom:1px solid #dceaf1} .table-filters{position:sticky;top:160px;z-index:8;background:#f7fafc;margin:0 -22px;padding:12px 22px;border-top:1px solid #dceaf1;border-bottom:1px solid #dceaf1} h1,h2{color:#12384e;font-weight:800}.section-title{font-size:1.35rem} .btn-primary,.btn-success{background:#1a96d3;border-color:#1a96d3;font-weight:700;border-radius:999px} .btn-primary:hover,.btn-success:hover{background:#157bb0;border-color:#157bb0} .btn-outline-primary{border-color:#1a96d3;color:#1a96d3;border-radius:999px;font-weight:700} .btn-outline-primary:hover{background:#1a96d3;border-color:#1a96d3} .form-control,.form-select{border-radius:8px}.table{vertical-align:middle} .badge-active{background:#25D366;color:#102417}.badge-off{background:#e7eef3;color:#51636e} .badge-status{background:#e8f4fb;color:#12628a}.badge-toggle{border:0;cursor:pointer}.badge-toggle:hover{filter:brightness(.96);transform:translateY(-1px)}.inline-edit-title{cursor:text;border-bottom:1px dotted #8ab8cf}.contact-card{background:#fff;border:1px solid #dceaf1;border-radius:8px;padding:14px} .badge-alert{background:#fff4d6;color:#7a4d00;border:1px solid #ffd978} .contacts-panel{background:#f7fafc;border-top:1px solid #dceaf1;border-bottom:1px solid #dceaf1;padding:18px 22px} .collapse-row td{background:#f7fafc}.client-logo{width:52px;height:52px;border-radius:8px;object-fit:cover;background:#eef7fc;border:1px solid #dceaf1} .muted{color:#61717a}.mini-img{width:42px;height:42px;border-radius:50%;object-fit:cover;background:#eef7fc} .signature-pad{width:100%;height:150px;border:1px solid #dceaf1;border-radius:8px;background:#fff;touch-action:none} .user-signature-pad{height:120px} .invoice-editor th,.invoice-editor td{white-space:nowrap}.invoice-editor input,.invoice-editor select{min-width:110px}.invoice-editor td:first-child input{min-width:260px} .order-tab-scroll{max-height:62vh;overflow-y:auto;overflow-x:hidden;padding-right:8px} .invoice-editor .discount-value-cell,.invoice-editor .discount-value-head{display:none}.invoice-editor.show-discount-values .discount-value-cell,.invoice-editor.show-discount-values .discount-value-head{display:table-cell} .invoice-row-hidden{display:none}.invoice-row-action,.content-row-action{width:38px;text-align:center}.btn-subtle{border-color:#dceaf1;color:#61717a;background:#fff;border-radius:999px}.btn-subtle:hover{border-color:#1a96d3;color:#1a96d3;background:#eef7fc} .marketing-checklist{max-height:260px;overflow:auto;border:1px solid #dceaf1;border-radius:8px;padding:10px;background:#fff} .admin-video{width:100%;max-height:70vh;background:transparent;border-radius:8px;display:block} #videoPreviewModal .modal-dialog{max-width:min(1120px,calc(100% - 32px))} #videoPreviewModal .modal-body{display:flex;justify-content:center;background:#fff;padding:12px} #videoPreviewModal.is-vertical .modal-dialog{max-width:min(430px,calc(100% - 32px))} #videoPreviewModal.is-vertical .admin-video{width:auto;height:min(76vh,calc((100vw - 64px) * 16 / 9));max-width:100%;max-height:76vh;object-fit:cover} .floating-player{position:fixed;left:50%;bottom:22px;transform:translateX(-50%) translateY(130%);z-index:1040;width:min(560px,calc(100% - 28px));background:rgba(255,255,255,.96);border:1px solid #dceaf1;border-radius:8px;box-shadow:0 18px 44px rgba(18,56,78,.22);padding:12px 14px;opacity:0;pointer-events:none;transition:transform .22s ease,opacity .22s ease;backdrop-filter:blur(10px)} .floating-player.is-visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto} .player-brand{width:42px;height:42px;border-radius:8px;background:#eef7fc;border:1px solid #dceaf1;object-fit:contain;padding:5px} .player-title{font-weight:800;color:#12384e;line-height:1.1}.player-subtitle{font-size:.8rem;color:#61717a} .player-icon-btn{width:36px;height:36px;border-radius:50%;border:1px solid #dceaf1;background:#fff;color:#1a96d3;display:inline-flex;align-items:center;justify-content:center} .player-icon-btn:hover{border-color:#1a96d3;background:#eef7fc}.player-icon-btn.primary{background:#1a96d3;color:#fff;border-color:#1a96d3} .player-progress{accent-color:#1a96d3;width:100%;height:4px}.player-time{font-size:.78rem;color:#61717a;min-width:42px;text-align:center} .modal-header{gap:16px}.modal-header .modal-title{margin-right:auto}.modal-actions{display:flex;align-items:center;gap:14px;flex-wrap:nowrap}.modal-actions .btn{white-space:nowrap}.modal-actions .btn-close{margin:0} /* Custom Alert Styles - eComercial Branding */ .alert{border-radius:8px;border:1px solid;padding:14px 18px;font-weight:600;display:flex;align-items:center;gap:12px} .alert::before{content:'';font-family:'Font Awesome 6 Free';font-weight:900;font-size:1.2rem;flex-shrink:0} .alert-success{background:#e8f8f0;border-color:#25D366;color:#0d5c2b} .alert-success::before{content:'\f058';color:#25D366} .alert-danger{background:#fee;border-color:#dc3545;color:#721c24} .alert-danger::before{content:'\f06a';color:#dc3545} .alert-warning{background:#fff4d6;border-color:#ffc107;color:#7a4d00} .alert-warning::before{content:'\f071';color:#ffc107} .alert-info{background:#e8f4fb;border-color:#1a96d3;color:#12628a} .alert-info::before{content:'\f05a';color:#1a96d3} .alert-primary{background:#e8f4fb;border-color:#1a96d3;color:#12628a} .alert-primary::before{content:'\f05a';color:#1a96d3} .alert .btn-close{margin-left:auto;opacity:.6} .alert .btn-close:hover{opacity:1} /* Sortable table headers */ .js-sortable{cursor:pointer;user-select:none;transition:color .15s ease} .js-sortable:hover{color:#1a96d3} .js-sortable:hover i{opacity:0.7 !important} /* User avatar styles */ .user-avatar{width:38px;height:38px;border-radius:50%;object-fit:cover;border:2px solid #dceaf1} .user-avatar-placeholder{width:38px;height:38px;border-radius:50%;background:#e8f4fb;border:2px solid #dceaf1;display:flex;align-items:center;justify-content:center;color:#1a96d3} .profile-photo-preview-wrap{display:flex;align-items:center;gap:10px;background:#f7fafc;border:1px solid #dceaf1;border-radius:8px;padding:8px} .email-logo-preview-wrap{display:flex;align-items:center;gap:10px;background:#f7fafc;border:1px solid #dceaf1;border-radius:8px;padding:8px} .email-logo-preview{width:52px;height:52px;border-radius:8px;object-fit:contain;background:#fff;border:1px solid #dceaf1;padding:4px} .email-preview-frame{width:100%;height:420px;border:1px solid #dceaf1;border-radius:8px;background:#fff} .user-name{color:#12384e;font-weight:600;font-size:0.9rem} .dropdown-toggle::after{margin-left:0.5rem} .dropdown-menu{border-radius:8px;border:1px solid #dceaf1;box-shadow:0 10px 24px rgba(18,56,78,.12);padding:8px 0} .dropdown-item{padding:10px 18px;color:#12384e;font-weight:600;transition:all .15s ease} .dropdown-item:hover{background:#e8f4fb;color:#1a96d3} .dropdown-item i{width:20px;text-align:center} .dropdown-divider{margin:8px 0;border-color:#dceaf1} /* Mobile Navigation */ .mobile-nav{display:none;position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:2px solid #dceaf1;box-shadow:0 -4px 12px rgba(18,56,78,.08);z-index:1000;padding:8px 0 calc(8px + env(safe-area-inset-bottom));height:calc(70px + env(safe-area-inset-bottom))} .mobile-nav-items{display:flex;justify-content:space-around;align-items:center;height:100%} .mobile-nav-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-decoration:none;color:#61717a;font-size:0.7rem;font-weight:600;padding:8px 4px;transition:all .2s ease;position:relative} .mobile-nav-item i{font-size:1.3rem;margin-bottom:4px;transition:all .2s ease} .mobile-nav-item.active{color:#1a96d3} .mobile-nav-item.active i{transform:scale(1.1)} .mobile-nav-item:active{transform:scale(0.95)} /* Floating Action Button */ .fab{position:fixed;bottom:calc(80px + env(safe-area-inset-bottom));right:20px;width:56px;height:56px;border-radius:50%;background:#1a96d3;color:#fff;border:none;box-shadow:0 4px 12px rgba(26,150,211,.4);display:none;align-items:center;justify-content:center;font-size:1.5rem;z-index:999;transition:all .3s ease} .fab:active{transform:scale(0.9)} .fab:hover{background:#157bb0;box-shadow:0 6px 16px rgba(26,150,211,.5)} /* Mobile Header */ .mobile-header{display:none;position:sticky;top:0;background:#fff;border-bottom:1px solid #dceaf1;padding:12px 16px;z-index:10;align-items:center;justify-content:space-between} .mobile-header-title{font-size:1.1rem;font-weight:800;color:#12384e;margin:0} .mobile-menu-btn{background:none;border:none;color:#1a96d3;font-size:1.3rem;padding:8px;cursor:pointer} /* Mobile Sidebar */ .mobile-sidebar{position:fixed;top:0;left:-100%;width:280px;height:100vh;background:#fff;box-shadow:4px 0 12px rgba(18,56,78,.15);z-index:1100;transition:left .3s ease;overflow-y:auto;padding-bottom:env(safe-area-inset-bottom)} .mobile-sidebar.open{left:0} .mobile-sidebar-header{padding:20px;background:linear-gradient(135deg,#1a96d3 0%,#157bb0 100%);color:#fff} .mobile-sidebar-user{display:flex;align-items:center;gap:12px;margin-bottom:16px} .mobile-sidebar-avatar{width:48px;height:48px;border-radius:50%;border:2px solid rgba(255,255,255,.3)} .mobile-sidebar-name{font-weight:700;font-size:1.1rem} .mobile-sidebar-username{font-size:0.85rem;opacity:0.9} .mobile-sidebar-menu{padding:8px 0} .mobile-sidebar-item{display:flex;align-items:center;gap:12px;padding:14px 20px;color:#12384e;text-decoration:none;font-weight:600;transition:all .2s ease} .mobile-sidebar-item:hover,.mobile-sidebar-item.active{background:#e8f4fb;color:#1a96d3} .mobile-sidebar-item i{width:24px;text-align:center;font-size:1.1rem} .mobile-sidebar-divider{height:1px;background:#dceaf1;margin:8px 0} .mobile-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(18,56,78,.5);z-index:1099;display:none;backdrop-filter:blur(2px)} .mobile-overlay.show{display:block} /* Mobile Optimizations */ @media (max-width:768px){ body{padding-bottom:calc(70px + env(safe-area-inset-bottom))} .admin-nav{display:none} .mobile-nav{display:block} .mobile-header{display:flex} .fab{display:flex} .container{padding-left:12px;padding-right:12px} .panel{padding:16px;margin-bottom:16px} .panel > .d-flex:first-child{position:static;margin:-16px -16px 16px;padding:16px;border-radius:8px 8px 0 0} .table-filters{position:static;margin:0 -16px 16px;padding:12px 16px} .section-title{font-size:1.2rem} .btn{padding:8px 16px;font-size:0.9rem} .btn-sm{padding:6px 12px;font-size:0.85rem} .table{font-size:0.85rem} .modal-dialog{margin:8px;max-width:calc(100% - 16px)} .modal-body{max-height:calc(100vh - 200px);overflow-y:auto} .form-label{font-size:0.9rem;margin-bottom:4px} .form-control,.form-select{font-size:0.9rem;padding:10px 12px} .dropdown-menu{max-width:calc(100vw - 32px)} /* Touch-friendly spacing */ .btn,.badge-toggle{min-height:44px;min-width:44px} .js-sortable{padding:12px 8px} /* Hide desktop elements */ .d-md-inline{display:none !important} } @media (max-width:576px){.modal-header{align-items:flex-start}.modal-actions{margin-left:0;width:100%;justify-content:space-between}} </style> </head> <body> <!-- Navigation --> <nav class="admin-nav py-3"> <div class="container d-flex align-items-center justify-content-between gap-3"> <a class="brand d-flex align-items-center gap-3 text-decoration-none" href="index.php"> <img src="<?= e($assetBase) ?>eComercial.png" alt="eComercial"> </a> <div class="d-flex align-items-center gap-3 flex-wrap justify-content-end"> <?php $navItems = [ 'dashboard' => ['icon' => 'fa-chart-pie', 'label' => 'Dashboard'], 'clients' => ['icon' => 'fa-building-user', 'label' => 'Clientes'], 'orders' => ['icon' => 'fa-clipboard-list', 'label' => 'Pedidos'], 'contents' => ['icon' => 'fa-list-ul', 'label' => 'Contenidos'], 'products' => ['icon' => 'fa-box-open', 'label' => 'Productos'], 'emails' => ['icon' => 'fa-envelope', 'label' => 'Correos'], 'web' => ['icon' => 'fa-globe', 'label' => 'Web'], 'users' => ['icon' => 'fa-users-gear', 'label' => 'Usuarios'], ]; foreach ($navItems as $section => $item): $isActive = $section === $currentSection; $btnClass = $isActive ? 'btn-primary' : 'btn-outline-primary'; ?> <a class="btn <?= $btnClass ?> btn-sm" href="?section=<?= e($section) ?>"> <i class="fa-solid <?= e($item['icon']) ?>"></i> <?= e($item['label']) ?> </a> <?php endforeach; ?> <a class="btn btn-outline-success btn-sm" href="../" target="_blank"> <i class="fa-solid fa-eye"></i> Ver sitio </a> <!-- User Menu Dropdown --> <?php $currentUser = admin_user(); $userPhoto = $currentUser['photo_url'] ?? ''; $userName = $currentUser['name'] ?? 'Usuario'; $userId = $currentUser['id'] ?? 0; ?> <div class="dropdown"> <button class="btn btn-link p-0 border-0 dropdown-toggle d-flex align-items-center gap-2" type="button" id="userMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false"> <?php if ($userPhoto): ?> <img class="user-avatar" src="<?= e(admin_asset_url($userPhoto)) ?>" alt="<?= e($userName) ?>"> <?php else: ?> <div class="user-avatar-placeholder"> <i class="fa-solid fa-user"></i> </div> <?php endif; ?> <span class="user-name d-none d-md-inline"><?= e($userName) ?></span> </button> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuDropdown"> <li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#profileEditModal"><i class="fa-solid fa-user-pen me-2"></i>Ver perfil</a></li> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item text-danger" href="logout.php"><i class="fa-solid fa-right-from-bracket me-2"></i>Cerrar sesión</a></li> </ul> </div> </div> </div> </nav> <!-- Mobile Header --> <div class="mobile-header"> <button class="mobile-menu-btn" type="button" id="mobileMenuBtn"> <i class="fa-solid fa-bars"></i> </button> <h1 class="mobile-header-title"><?= e($pageTitle) ?></h1> <?php $currentUser = admin_user(); $userPhoto = $currentUser['photo_url'] ?? ''; ?> <?php if ($userPhoto): ?> <img class="user-avatar" src="<?= e(admin_asset_url($userPhoto)) ?>" alt="Usuario" style="width:32px;height:32px;"> <?php else: ?> <div class="user-avatar-placeholder" style="width:32px;height:32px;font-size:0.9rem;"> <i class="fa-solid fa-user"></i> </div> <?php endif; ?> </div> <!-- Mobile Sidebar --> <div class="mobile-overlay" id="mobileOverlay"></div> <div class="mobile-sidebar" id="mobileSidebar"> <div class="mobile-sidebar-header"> <div class="mobile-sidebar-user"> <?php if ($userPhoto): ?> <img class="mobile-sidebar-avatar" src="<?= e(admin_asset_url($userPhoto)) ?>" alt="<?= e($currentUser['name'] ?? 'Usuario') ?>"> <?php else: ?> <div class="mobile-sidebar-avatar" style="background:#fff;display:flex;align-items:center;justify-content:center;"> <i class="fa-solid fa-user" style="color:#1a96d3;"></i> </div> <?php endif; ?> <div> <div class="mobile-sidebar-name"><?= e($currentUser['name'] ?? 'Usuario') ?></div> <div class="mobile-sidebar-username">@<?= e($currentUser['username'] ?? 'usuario') ?></div> </div> </div> </div> <div class="mobile-sidebar-menu"> <a class="mobile-sidebar-item" href="#" data-bs-toggle="modal" data-bs-target="#profileEditModal"> <i class="fa-solid fa-user-pen"></i> <span>Mi Perfil</span> </a> <a class="mobile-sidebar-item" href="../" target="_blank"> <i class="fa-solid fa-eye"></i> <span>Ver Sitio Público</span> </a> <div class="mobile-sidebar-divider"></div> <a class="mobile-sidebar-item text-danger" href="logout.php"> <i class="fa-solid fa-right-from-bracket"></i> <span>Cerrar Sesión</span> </a> </div> </div> <!-- Mobile Bottom Navigation --> <nav class="mobile-nav"> <div class="mobile-nav-items"> <a class="mobile-nav-item <?= $currentSection === 'dashboard' ? 'active' : '' ?>" href="?section=dashboard"> <i class="fa-solid fa-chart-pie"></i> <span>Dashboard</span> </a> <a class="mobile-nav-item <?= $currentSection === 'clients' ? 'active' : '' ?>" href="?section=clients"> <i class="fa-solid fa-building-user"></i> <span>Clientes</span> </a> <a class="mobile-nav-item <?= $currentSection === 'orders' ? 'active' : '' ?>" href="?section=orders"> <i class="fa-solid fa-clipboard-list"></i> <span>Pedidos</span> </a> <a class="mobile-nav-item <?= $currentSection === 'contents' ? 'active' : '' ?>" href="?section=contents"> <i class="fa-solid fa-list-ul"></i> <span>Contenidos</span> </a> <a class="mobile-nav-item <?= $currentSection === 'products' ? 'active' : '' ?>" href="?section=products"> <i class="fa-solid fa-box-open"></i> <span>Productos</span> </a> <a class="mobile-nav-item <?= $currentSection === 'emails' ? 'active' : '' ?>" href="?section=emails"> <i class="fa-solid fa-envelope"></i> <span>Correos</span> </a> <a class="mobile-nav-item <?= $currentSection === 'users' ? 'active' : '' ?>" href="?section=users"> <i class="fa-solid fa-users-gear"></i> <span>Usuarios</span> </a> </div> </nav> <!-- Floating Action Button --> <button class="fab" id="fabBtn" type="button"> <i class="fa-solid fa-plus"></i> </button> <!-- Main Content --> <main class="container py-4"> <!-- Flash Messages --> <?php if ($flash): ?> <div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert"> <?= e($flash['message']) ?> <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> <?php endif; ?> <!-- Section Content --> <?= $sectionContent ?> <!-- Current User Profile Modal --> <div class="modal fade" id="profileEditModal" tabindex="-1" aria-labelledby="profileEditModalLabel" aria-hidden="true"> <div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-content"> <form method="post" enctype="multipart/form-data"> <div class="modal-header"> <h2 class="modal-title fs-5" id="profileEditModalLabel">Ver perfil</h2> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button> </div> <div class="modal-body"> <div class="row g-3"> <input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>"> <input type="hidden" name="action" value="save_user"> <input type="hidden" name="id" value="<?= e($currentUser['id'] ?? 0) ?>"> <input type="hidden" name="is_active" value="1"> <div class="col-md-6"><label class="form-label">Nombre</label><input class="form-control" name="name" value="<?= e($currentUser['name'] ?? '') ?>" required></div> <div class="col-md-6"><label class="form-label">Usuario</label><input class="form-control" name="username" value="<?= e($currentUser['username'] ?? '') ?>" required autocomplete="username"></div> <div class="col-md-6"> <label class="form-label">Nueva clave</label> <div class="input-group"> <input class="form-control" type="password" name="password" autocomplete="new-password"> <button class="btn btn-outline-primary js-toggle-password" type="button" aria-label="Ver clave"><i class="fa-solid fa-eye"></i></button> </div> </div> <div class="col-md-6"> <label class="form-label">Foto de perfil</label> <input class="form-control mb-2" name="photo_url" value="<?= e($userPhoto) ?>" placeholder="https://"> <div class="profile-photo-preview-wrap mb-2 <?= $userPhoto ? '' : 'd-none' ?>"> <img class="profile-photo-preview mini-img" src="<?= e(admin_asset_url($userPhoto)) ?>" alt="Foto actual"> <span class="muted small">Foto actual</span> </div> <input class="form-control" type="file" name="photo_file" accept="image/*"> </div> <div class="col-12"> <label class="form-label">Firma</label> <input type="hidden" name="signature_url" value="<?= e($currentUser['signature_url'] ?? '') ?>"> <canvas class="signature-pad user-signature-pad" width="420" height="120"></canvas> <div class="d-flex gap-2 flex-wrap mt-2"> <button class="btn btn-sm btn-outline-primary js-clear-signature" type="button">Limpiar firma</button> <input class="form-control form-control-sm js-signature-file" type="file" accept="image/*"> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button> <button class="btn btn-primary">Guardar perfil</button> </div> </form> </div> </div> </div> <!-- Video Preview Modal --> <div class="modal fade" id="videoPreviewModal" tabindex="-1" aria-labelledby="videoPreviewModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h2 class="modal-title fs-5" id="videoPreviewModalLabel">Vista previa de video</h2> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button> </div> <div class="modal-body"> <video class="admin-video" controls playsinline></video> </div> </div> </div> </div> <!-- Floating Audio Player --> <div class="floating-player" id="adminAudioPlayer" aria-live="polite"> <div class="d-flex align-items-center gap-3"> <img class="player-brand" src="<?= e($assetBase) ?>eComercial.png" alt="eComercial"> <div class="flex-grow-1 min-w-0"> <div class="d-flex align-items-center justify-content-between gap-2"> <div class="min-w-0"> <div class="player-title text-truncate" id="adminAudioTitle">Audio</div> <div class="player-subtitle">eComercial admin</div> </div> <div class="d-flex align-items-center gap-2"> <button class="player-icon-btn primary" type="button" id="adminAudioToggle" aria-label="Pausar"><i class="fa-solid fa-pause"></i></button> <button class="player-icon-btn" type="button" id="adminAudioClose" aria-label="Cerrar reproductor"><i class="fa-solid fa-xmark"></i></button> </div> </div> <div class="d-flex align-items-center gap-2 mt-2"> <span class="player-time" id="adminAudioCurrent">0:00</span> <input class="player-progress" id="adminAudioProgress" type="range" min="0" max="100" value="0" step="0.1" aria-label="Progreso"> <span class="player-time" id="adminAudioDuration">0:00</span> </div> </div> </div> </div> <!-- Custom Confirmation Modal --> <div class="modal fade" id="confirmModal" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true" data-bs-backdrop="static"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header border-0 pb-0"> <img src="<?= e($assetBase) ?>eComercial.png" alt="eComercial" style="height: 40px; width: auto;"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button> </div> <div class="modal-body pt-3"> <p class="mb-0" id="confirmModalMessage"></p> </div> <div class="modal-footer border-0" id="confirmModalFooter"> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="confirmModalCancel">Cancelar</button> <button type="button" class="btn btn-primary" id="confirmModalAccept">Aceptar</button> </div> </div> </div> </div> <!-- Delete Order with Options Modal --> <div class="modal fade" id="deleteOrderModal" tabindex="-1" aria-labelledby="deleteOrderModalLabel" aria-hidden="true" data-bs-backdrop="static"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header border-0 pb-0"> <img src="<?= e($assetBase) ?>eComercial.png" alt="eComercial" style="height: 40px; width: auto;"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button> </div> <div class="modal-body pt-3"> <p class="mb-3" id="deleteOrderModalMessage"></p> <p class="mb-2"><strong>¿Qué deseas hacer con los contenidos?</strong></p> </div> <div class="modal-footer border-0 flex-column gap-2"> <button type="button" class="btn btn-primary w-100" id="deleteOrderKeepContents"> <i class="fa-solid fa-box-archive me-2"></i>Pasar a catálogo </button> <button type="button" class="btn btn-outline-danger w-100" id="deleteOrderDeleteContents"> <i class="fa-solid fa-trash me-2"></i>Eliminar contenidos también </button> <button type="button" class="btn btn-outline-secondary w-100" data-bs-dismiss="modal">Cancelar</button> </div> </div> </div> </div> </main> <!-- JavaScript Resources --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <!-- Custom Confirm Dialog --> <script> // Override native confirm with custom modal (function() { var confirmModal = null; var confirmModalMessage = null; var confirmModalAccept = null; var confirmModalCancel = null; var confirmCallback = null; document.addEventListener('DOMContentLoaded', function() { confirmModal = new bootstrap.Modal(document.getElementById('confirmModal')); confirmModalMessage = document.getElementById('confirmModalMessage'); confirmModalAccept = document.getElementById('confirmModalAccept'); confirmModalCancel = document.getElementById('confirmModalCancel'); confirmModalAccept.addEventListener('click', function() { confirmModal.hide(); if (confirmCallback) { confirmCallback(true); confirmCallback = null; } }); confirmModalCancel.addEventListener('click', function() { confirmModal.hide(); if (confirmCallback) { confirmCallback(false); confirmCallback = null; } }); // Handle modal close without action document.getElementById('confirmModal').addEventListener('hidden.bs.modal', function() { if (confirmCallback) { confirmCallback(false); confirmCallback = null; } }); // Setup delete order modal var deleteOrderModal = new bootstrap.Modal(document.getElementById('deleteOrderModal')); var deleteOrderModalMessage = document.getElementById('deleteOrderModalMessage'); var deleteOrderKeepContents = document.getElementById('deleteOrderKeepContents'); var deleteOrderDeleteContents = document.getElementById('deleteOrderDeleteContents'); var currentOrderForm = null; deleteOrderKeepContents.addEventListener('click', function() { deleteOrderModal.hide(); if (currentOrderForm) { var input = document.createElement('input'); input.type = 'hidden'; input.name = 'delete_contents'; input.value = '0'; currentOrderForm.appendChild(input); currentOrderForm.submit(); currentOrderForm = null; } }); deleteOrderDeleteContents.addEventListener('click', function() { deleteOrderModal.hide(); if (currentOrderForm) { var input = document.createElement('input'); input.type = 'hidden'; input.name = 'delete_contents'; input.value = '1'; currentOrderForm.appendChild(input); currentOrderForm.submit(); currentOrderForm = null; } }); document.getElementById('deleteOrderModal').addEventListener('hidden.bs.modal', function() { currentOrderForm = null; }); // Handle delete client forms document.querySelectorAll('.js-delete-client-form button[type="button"]').forEach(function(button) { button.addEventListener('click', function(e) { e.preventDefault(); var form = button.closest('form'); var clientName = form.dataset.clientName || 'este cliente'; var ordersCount = parseInt(form.dataset.ordersCount) || 0; var contentsCount = parseInt(form.dataset.contentsCount) || 0; var contactsCount = parseInt(form.dataset.contactsCount) || 0; var message = '¿Eliminar cliente "' + clientName + '"?'; var deleteItems = []; var catalogItems = []; if (ordersCount > 0) { deleteItems.push(ordersCount + ' pedido' + (ordersCount !== 1 ? 's' : '')); } if (contactsCount > 0) { deleteItems.push(contactsCount + ' contacto' + (contactsCount !== 1 ? 's' : '')); } if (deleteItems.length > 0) { message += '\n\nSe eliminarán: ' + deleteItems.join(', ') + '.'; } if (contentsCount > 0) { message += '\n\n' + contentsCount + ' contenido' + (contentsCount !== 1 ? 's se pasarán' : ' se pasará') + ' a catálogo.'; } customConfirm(message, function(result) { if (result) { form.submit(); } }); }); }); // Handle delete order forms document.querySelectorAll('.js-delete-order-form button[type="button"]').forEach(function(button) { button.addEventListener('click', function(e) { e.preventDefault(); var form = button.closest('form'); var orderTitle = form.dataset.orderTitle || 'este pedido'; var contentsCount = parseInt(form.dataset.contentsCount) || 0; if (contentsCount > 0) { var message = '¿Eliminar pedido "' + orderTitle + '"?\n\n'; message += 'Este pedido tiene ' + contentsCount + ' contenido' + (contentsCount !== 1 ? 's' : '') + '.'; deleteOrderModalMessage.innerHTML = message.replace(/\n/g, '<br>'); currentOrderForm = form; deleteOrderModal.show(); } else { customConfirm('¿Eliminar pedido "' + orderTitle + '"?', function(result) { if (result) { form.submit(); } }); } }); }); }); // Custom confirm function window.customConfirm = function(message, callback) { if (!confirmModal) { // Fallback to native confirm if modal not ready callback(confirm(message)); return; } // Replace \n with <br> for HTML display confirmModalMessage.innerHTML = message.replace(/\n/g, '<br>'); confirmCallback = callback; confirmModal.show(); }; // Intercept form submissions with onsubmit="return confirm(...)" document.addEventListener('submit', function(e) { var form = e.target; var onsubmit = form.getAttribute('onsubmit'); if (onsubmit && onsubmit.includes('confirm(')) { e.preventDefault(); // Extract message from confirm() var match = onsubmit.match(/confirm\(['"](.+?)['"]\)/); if (match && match[1]) { customConfirm(match[1], function(result) { if (result) { // Remove onsubmit to avoid loop form.removeAttribute('onsubmit'); form.submit(); } }); } } }, true); })(); </script> <!-- Inline scripts --> <script> function fillFormFromDataset(form, dataset, fields) { fields.forEach(function (field) { var input = form.querySelector('[name="' + field.name + '"]'); if (!input) return; var value = dataset[field.data] || ''; if (input.type === 'checkbox') { input.checked = value === '1'; return; } input.value = value; }); } function resolveAdminAssetUrl(path) { path = String(path || '').trim(); if (!path) return ''; if (/^(https?:)?\/\//.test(path) || path.indexOf('data:') === 0 || path.charAt(0) === '/') { return path; } return '<?= e($basePath) ?>/' + path.replace(/^\/+/, ''); } function updatePhotoPreview(form) { if (!form) return; var input = form.querySelector('[name="photo_url"]'); var wrap = form.querySelector('.profile-photo-preview-wrap'); var image = form.querySelector('.profile-photo-preview'); if (!input || !wrap || !image) return; var value = input.value.trim(); if (value) { image.src = resolveAdminAssetUrl(value); wrap.classList.remove('d-none'); } else { image.removeAttribute('src'); wrap.classList.add('d-none'); } } function listValues(value) { return String(value || '').split(',').map(function (item) { return item.trim(); }).filter(Boolean); } function updateMarketingHiddenInput(group) { var field = group.dataset.field; var hidden = group.closest('form').querySelector('input[type="hidden"][name="' + field + '"]'); if (!hidden) return; var values = Array.from(group.querySelectorAll('.js-marketing-option:checked')).map(function (checkbox) { return checkbox.value; }); var other = group.querySelector('.js-marketing-other'); if (other && other.value.trim()) { values.push(other.value.trim()); } hidden.value = values.join(', '); var selectAll = group.querySelector('.js-marketing-select-all'); var options = Array.from(group.querySelectorAll('.js-marketing-option')); if (selectAll) { selectAll.checked = options.length > 0 && options.every(function (checkbox) { return checkbox.checked; }); } } function syncMarketingChecklist(group) { var field = group.dataset.field; var hidden = group.closest('form').querySelector('input[type="hidden"][name="' + field + '"]'); var selected = listValues(hidden ? hidden.value : ''); var knownValues = Array.from(group.querySelectorAll('.js-marketing-option')).map(function (checkbox) { return checkbox.value; }); group.querySelectorAll('.js-marketing-option').forEach(function (checkbox) { checkbox.checked = selected.includes(checkbox.value); }); var other = group.querySelector('.js-marketing-other'); if (other) { other.value = selected.filter(function (value) { return !knownValues.includes(value); }).join(', '); } updateMarketingHiddenInput(group); } function syncMarketingChecklists(form) { form.querySelectorAll('.marketing-checklist').forEach(syncMarketingChecklist); } function updatePurposeOptions(form) { var selects = Array.from(form.querySelectorAll('.js-purpose-select')); if (!form.dataset.purposeOptions && selects[0]) { form.dataset.purposeOptions = JSON.stringify(Array.from(selects[0].options).map(function (option) { return option.value; }).filter(Boolean)); } var optionValues = form.dataset.purposeOptions ? JSON.parse(form.dataset.purposeOptions) : []; var used = []; selects.forEach(function (select) { if (select.value && used.includes(select.value)) { select.value = ''; } if (select.value) used.push(select.value); }); var selectedValues = selects.map(function (select) { return select.value; }).filter(Boolean); selects.forEach(function (select) { var currentValue = select.value; select.innerHTML = '<option value="">Seleccione...</option>'; optionValues.forEach(function (value) { if (value === currentValue || !selectedValues.includes(value)) { var option = document.createElement('option'); option.value = value; option.textContent = value; select.appendChild(option); } }); select.value = currentValue; }); } function initMarketingControls() { document.querySelectorAll('.marketing-checklist').forEach(function (group) { syncMarketingChecklist(group); group.querySelectorAll('.js-marketing-option').forEach(function (checkbox) { checkbox.addEventListener('change', function () { updateMarketingHiddenInput(group); }); }); var selectAll = group.querySelector('.js-marketing-select-all'); if (selectAll) { selectAll.addEventListener('change', function () { group.querySelectorAll('.js-marketing-option').forEach(function (checkbox) { checkbox.checked = selectAll.checked; }); updateMarketingHiddenInput(group); }); } var other = group.querySelector('.js-marketing-other'); if (other) { other.addEventListener('input', function () { updateMarketingHiddenInput(group); }); } }); document.querySelectorAll('form').forEach(function (form) { updatePurposeOptions(form); form.querySelectorAll('.js-purpose-select').forEach(function (select) { select.addEventListener('change', function () { updatePurposeOptions(form); }); }); }); } function fillInvoiceRows(form, rows) { var lineRows = form.querySelectorAll('.js-invoice-row'); lineRows.forEach(function (lineRow, index) { var row = rows[index] || {}; var hasRowData = !!(row.description || row.title || row.unit_price); lineRow.classList.toggle('invoice-row-hidden', index >= 3 && !hasRowData); var productId = lineRow.querySelector('[name="invoice_product_id[]"]'); var title = lineRow.querySelector('[name="invoice_title[]"]'); var genre = lineRow.querySelector('[name="invoice_genre[]"]'); var mediaUrl = lineRow.querySelector('[name="invoice_media_url[]"]'); var description = lineRow.querySelector('[name="invoice_description[]"]'); var quantity = lineRow.querySelector('[name="invoice_quantity[]"]'); var unitPrice = lineRow.querySelector('[name="invoice_unit_price[]"]'); var discountType = lineRow.querySelector('[name="invoice_discount_type[]"]'); var discountValue = lineRow.querySelector('[name="invoice_discount_value[]"]'); if (productId) productId.value = row.product_id || ''; if (title) title.value = row.title || row.description || ''; if (genre) genre.value = row.genre || ''; if (mediaUrl) mediaUrl.value = row.media_url || ''; if (description) description.value = row.description || row.title || ''; if (quantity) quantity.value = row.quantity ? String(Math.max(1, Math.floor(parseFloat(row.quantity) || 1))) : (index === 0 ? '1' : ''); if (unitPrice) unitPrice.value = row.unit_price || ''; if (discountType) discountType.value = row.discount_type || 'none'; if (discountValue) discountValue.value = row.discount_value || ''; setRowDiscountVisible(lineRow, row.discount_type && row.discount_type !== 'none'); }); updateDiscountColumns(form); calculateInvoice(form); } function clearInvoiceRow(lineRow) { lineRow.querySelectorAll('input').forEach(function (input) { if (input.type === 'file') { input.value = ''; } else if (input.name === 'invoice_quantity[]') { input.value = ''; } else { input.value = ''; } }); lineRow.querySelectorAll('select').forEach(function (select) { select.selectedIndex = 0; }); setRowDiscountVisible(lineRow, false); var lineTotal = lineRow.querySelector('.js-line-total'); if (lineTotal) lineTotal.textContent = '$ 0'; } function fillContentRows(form, rows) { var contentRows = form.querySelectorAll('.content-editor tbody tr'); contentRows.forEach(function (contentRow, index) { var row = rows[index] || {}; var hasRowData = !!(row.id || row.title || row.product_id || row.media_url); contentRow.classList.toggle('invoice-row-hidden', index >= 3 && !hasRowData); var id = contentRow.querySelector('[name="content_id[]"]'); var product = contentRow.querySelector('[name="content_product_id[]"]'); var type = contentRow.querySelector('[name="content_item_type[]"]'); var title = contentRow.querySelector('[name="content_title[]"]'); var genre = contentRow.querySelector('[name="content_genre[]"]'); var media = contentRow.querySelector('[name="content_media_url[]"]'); var action = contentRow.querySelector('[name="content_action_url[]"]'); var sort = contentRow.querySelector('[name="content_sort_order[]"]'); var active = contentRow.querySelector('.js-content-active') || contentRow.querySelector('input[type="checkbox"]'); var download = contentRow.querySelector('.js-content-download'); if (id) id.value = row.id || ''; if (product) product.value = row.product_id || ''; if (type) type.value = product && product.value ? (row.item_type || 'audio') : ''; if (title) title.value = row.title || ''; if (genre) genre.value = row.genre || ''; if (media) media.value = row.media_url || ''; if (action) action.value = row.action_url || ''; if (sort) sort.value = row.sort_order || index; if (active) active.checked = row.is_active === undefined ? false : String(row.is_active) === '1'; if (download) download.checked = row.allow_download === undefined ? false : String(row.allow_download) === '1'; var company = contentRow.querySelector('[name="content_company[]"]'); if (company) company.value = row.company || row.title || ''; }); } function clearContentRow(contentRow) { contentRow.querySelectorAll('input').forEach(function (input) { if (input.type === 'checkbox') { input.checked = false; } else { input.value = ''; } }); contentRow.querySelectorAll('select').forEach(function (select) { select.selectedIndex = 0; }); } function visibleInvoiceRows(table) { return Array.from(table.querySelectorAll('.js-invoice-row')).filter(function (row) { return !row.classList.contains('invoice-row-hidden'); }); } function visibleContentRows(table) { return Array.from(table.querySelectorAll('.js-content-row')).filter(function (row) { return !row.classList.contains('invoice-row-hidden'); }); } function setRowDiscountVisible(lineRow, visible) { var button = lineRow.querySelector('.js-toggle-discount'); var discountType = lineRow.querySelector('[name="invoice_discount_type[]"]'); var discountValue = lineRow.querySelector('[name="invoice_discount_value[]"]'); if (discountType) { discountType.classList.toggle('d-none', !visible); if (!visible) discountType.value = 'none'; if (visible && discountType.value === 'none') discountType.value = 'percent'; } if (discountValue) { discountValue.classList.toggle('d-none', !visible); if (!visible) discountValue.value = ''; } if (button) { button.classList.toggle('btn-primary', visible); button.classList.toggle('btn-outline-primary', !visible); } } function updateDiscountColumns(form) { form.querySelectorAll('.invoice-editor').forEach(function (table) { var hasDiscount = Array.from(table.querySelectorAll('[name="invoice_discount_type[]"]')).some(function (select) { return select.value && select.value !== 'none'; }); table.classList.toggle('show-discount-values', hasDiscount); }); } function formatCurrency(value) { return '$ ' + Math.round(value || 0).toLocaleString('es-CO'); } function numberValue(input) { return Math.max(0, parseFloat(String(input && input.value ? input.value : '0').replace(',', '.')) || 0); } function quantityValue(input) { return Math.max(1, Math.floor(numberValue(input)) || 1); } function calculateInvoice(form) { var subtotal = 0; var itemDiscounts = 0; form.querySelectorAll('.js-invoice-row').forEach(function (lineRow) { var title = lineRow.querySelector('[name="invoice_title[]"]'); var description = lineRow.querySelector('[name="invoice_description[]"]'); if (description && title) description.value = title.value; var quantity = quantityValue(lineRow.querySelector('[name="invoice_quantity[]"]')); var unitPrice = numberValue(lineRow.querySelector('[name="invoice_unit_price[]"]')); var lineSubtotal = quantity * unitPrice; var discountType = lineRow.querySelector('[name="invoice_discount_type[]"]'); var discountValue = numberValue(lineRow.querySelector('[name="invoice_discount_value[]"]')); var discount = 0; if (discountType && discountType.value === 'percent') { discount = lineSubtotal * Math.min(100, discountValue) / 100; } else if (discountType && discountType.value === 'amount') { discount = Math.min(lineSubtotal, discountValue); } subtotal += lineSubtotal; itemDiscounts += discount; var lineTotal = lineRow.querySelector('.js-line-total'); if (lineTotal) lineTotal.textContent = formatCurrency(Math.max(0, lineSubtotal - discount)); }); var orderDiscountInput = numberValue(form.querySelector('[name="order_discount"]')); var orderDiscountType = (form.querySelector('[name="order_discount_type"]') || {}).value || 'amount'; var discountBase = Math.max(0, subtotal - itemDiscounts); var orderDiscount = orderDiscountType === 'percent' ? discountBase * Math.min(100, orderDiscountInput) / 100 : Math.min(orderDiscountInput, discountBase); var taxRate = numberValue(form.querySelector('[name="tax_rate"]')); var taxable = Math.max(0, subtotal - itemDiscounts - orderDiscount); var total = taxable + (taxable * taxRate / 100); updateDiscountColumns(form); var subtotalOutput = form.querySelector('.js-invoice-subtotal'); if (subtotalOutput) subtotalOutput.value = formatCurrency(subtotal); var output = form.querySelector('.js-invoice-total'); if (output) output.value = formatCurrency(total); } function applyProductPrice(select, force) { var row = select.closest('.js-invoice-row'); if (!row) return; var unitPrice = row.querySelector('[name="invoice_unit_price[]"]'); var discountType = row.querySelector('[name="invoice_discount_type[]"]'); var discountValue = row.querySelector('[name="invoice_discount_value[]"]'); var option = select.selectedOptions && select.selectedOptions[0] ? select.selectedOptions[0] : null; var price = option ? option.dataset.price : ''; if (unitPrice && price && (force || !unitPrice.value)) { unitPrice.value = price; } if (option && discountType && discountValue) { discountType.value = option.dataset.discountType || 'none'; discountValue.value = option.dataset.discountValue || ''; setRowDiscountVisible(row, discountType.value !== 'none'); } } function applyContentProductType(select) { var option = select.selectedOptions && select.selectedOptions[0] ? select.selectedOptions[0] : null; var itemType = option ? option.dataset.itemType : ''; var scope = select.closest('.js-content-row') || select.closest('form'); var type = scope ? scope.querySelector('[name="content_item_type[]"], [name="item_type"]') : null; if (type) type.value = itemType || ''; var title = scope ? scope.querySelector('[name="content_title[]"], [name="title"]') : null; var optionTitle = option ? (option.dataset.contentTitle || option.textContent.split('/').pop().trim()) : ''; if (title && optionTitle && !title.value) { title.value = optionTitle; } } function normalizeFilterText(value) { return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim(); } function applyTableFilters(group) { var table = document.getElementById(group.dataset.filterTarget || ''); if (!table) return; var search = normalizeFilterText((group.querySelector('.js-table-search') || {}).value || ''); var selects = Array.from(group.querySelectorAll('.js-table-select')); table.querySelectorAll('[data-filter-row]').forEach(function (row) { var matchesSearch = !search || normalizeFilterText(row.textContent).includes(search); var matchesSelects = selects.every(function (select) { var value = normalizeFilterText(select.value); if (!value) return true; return normalizeFilterText(row.dataset[select.dataset.filterKey] || '') === value; }); var visible = matchesSearch && matchesSelects; row.style.display = visible ? '' : 'none'; var collapseRow = row.nextElementSibling; if (collapseRow && collapseRow.classList.contains('collapse-row')) { collapseRow.style.display = visible ? '' : 'none'; } }); } function initTableFilters() { document.querySelectorAll('.table-filters').forEach(function (group) { group.querySelectorAll('.js-table-search').forEach(function (input) { input.addEventListener('input', function () { applyTableFilters(group); }); }); group.querySelectorAll('.js-table-select').forEach(function (select) { select.addEventListener('change', function () { applyTableFilters(group); }); }); applyTableFilters(group); }); } function clearSignatureCanvas(canvas) { var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height); canvas.dataset.touched = ''; } function loadSignatureCanvas(canvas, value) { clearSignatureCanvas(canvas); if (!value) return; var img = new Image(); img.onload = function () { var ratio = Math.min(canvas.width / img.width, canvas.height / img.height); var width = img.width * ratio; var height = img.height * ratio; canvas.getContext('2d').drawImage(img, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height); }; img.src = value; } function loadSignatureCanvases(form) { form.querySelectorAll('.signature-pad').forEach(function (canvas) { // Check if it's a company signature canvas with data-signature-url if (canvas.classList.contains('js-company-signature-canvas')) { var signatureUrl = canvas.dataset.signatureUrl; if (signatureUrl) { loadSignatureCanvas(canvas, signatureUrl); canvas.dataset.touched = '1'; // Mark as touched so it's not editable } return; } // Regular signature canvas (client signature) var hidden = canvas.previousElementSibling; loadSignatureCanvas(canvas, hidden && hidden.value ? hidden.value : ''); }); } function initSignaturePads() { document.querySelectorAll('.signature-pad').forEach(function (canvas) { // Skip company signature canvases (read-only) if (canvas.classList.contains('js-company-signature-canvas')) { return; } clearSignatureCanvas(canvas); var drawing = false; var ctx = canvas.getContext('2d'); ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.strokeStyle = '#12384e'; function point(event) { var rect = canvas.getBoundingClientRect(); var source = event.touches ? event.touches[0] : event; return { x: (source.clientX - rect.left) * (canvas.width / rect.width), y: (source.clientY - rect.top) * (canvas.height / rect.height) }; } function start(event) { event.preventDefault(); drawing = true; canvas.dataset.touched = '1'; var p = point(event); ctx.beginPath(); ctx.moveTo(p.x, p.y); } function move(event) { if (!drawing) return; event.preventDefault(); var p = point(event); ctx.lineTo(p.x, p.y); ctx.stroke(); } function end() { drawing = false; } canvas.addEventListener('mousedown', start); canvas.addEventListener('mousemove', move); window.addEventListener('mouseup', end); canvas.addEventListener('touchstart', start, {passive: false}); canvas.addEventListener('touchmove', move, {passive: false}); canvas.addEventListener('touchend', end); }); document.querySelectorAll('.js-clear-signature').forEach(function (button) { button.addEventListener('click', function () { var wrap = button.closest('.col-12, .col-md-6') || button.parentElement; var canvas = wrap.querySelector('.signature-pad'); var hidden = wrap.querySelector('input[type="hidden"]'); if (canvas) clearSignatureCanvas(canvas); if (hidden) hidden.value = ''; }); }); document.querySelectorAll('.js-signature-file').forEach(function (input) { input.addEventListener('change', function () { var file = input.files && input.files[0] ? input.files[0] : null; if (!file) return; var wrap = input.closest('.col-12, .col-md-6') || input.parentElement; var hidden = wrap.querySelector('input[type="hidden"]'); var canvas = wrap.querySelector('.signature-pad'); var reader = new FileReader(); reader.onload = function () { if (hidden) hidden.value = reader.result; if (canvas) { canvas.dataset.touched = ''; loadSignatureCanvas(canvas, reader.result); } }; reader.readAsDataURL(file); }); }); document.querySelectorAll('form').forEach(function (form) { form.addEventListener('submit', function (event) { if (form.dataset.submitting === '1') { event.preventDefault(); return; } form.dataset.submitting = '1'; form.querySelectorAll('button[type="submit"], button:not([type])').forEach(function (button) { if (button.closest('.modal-header') || button.classList.contains('btn-primary')) { button.disabled = true; if (!button.dataset.originalText) button.dataset.originalText = button.textContent; button.textContent = 'Guardando...'; } }); form.querySelectorAll('.marketing-checklist').forEach(updateMarketingHiddenInput); calculateInvoice(form); var objective = form.querySelector('[name="marketing_objective"]'); if (objective) { objective.value = [ form.querySelector('[name="marketing_purpose_1"]') ? form.querySelector('[name="marketing_purpose_1"]').value : '', form.querySelector('[name="marketing_purpose_2"]') ? form.querySelector('[name="marketing_purpose_2"]').value : '', form.querySelector('[name="marketing_purpose_3"]') ? form.querySelector('[name="marketing_purpose_3"]').value : '' ].filter(Boolean).join(' / '); } var audience = form.querySelector('[name="marketing_audience"]'); if (audience) { audience.value = [ form.querySelector('[name="marketing_location"]') ? form.querySelector('[name="marketing_location"]').value : '', form.querySelector('[name="marketing_gender"]') ? form.querySelector('[name="marketing_gender"]').value : '', form.querySelector('[name="marketing_ages"]') ? form.querySelector('[name="marketing_ages"]').value : '', form.querySelector('[name="marketing_socioeconomic"]') ? form.querySelector('[name="marketing_socioeconomic"]').value : '' ].filter(Boolean).join(' / '); } var references = form.querySelector('[name="marketing_references"]'); if (references) { references.value = [ form.querySelector('[name="marketing_inspirations"]') ? form.querySelector('[name="marketing_inspirations"]').value : '', form.querySelector('[name="marketing_music_genres"]') ? form.querySelector('[name="marketing_music_genres"]').value : '' ].filter(Boolean).join(' / '); } form.querySelectorAll('.signature-pad').forEach(function (canvas) { var hidden = canvas.previousElementSibling; if (!hidden) return; if (canvas.dataset.touched === '1') { hidden.value = canvas.toDataURL('image/png'); } }); }); }); } // Initialize all controls on page load initMarketingControls(); initTableFilters(); initSignaturePads(); </script> <!-- Content Management Scripts --> <script> // Edit product modal population document.querySelectorAll('.js-edit-product').forEach(function (button) { button.addEventListener('click', function () { var form = document.querySelector('#productEditModal form'); fillFormFromDataset(form, button.dataset, [ {name: 'id', data: 'id'}, {name: 'tab_id', data: 'tabId'}, {name: 'title', data: 'title'}, {name: 'description', data: 'description'}, {name: 'price_label', data: 'priceLabel'}, {name: 'price_value', data: 'priceValue'}, {name: 'discount_type', data: 'discountType'}, {name: 'discount_value', data: 'discountValue'}, {name: 'cta_label', data: 'ctaLabel'}, {name: 'cta_url', data: 'ctaUrl'}, {name: 'sort_order', data: 'sortOrder'}, {name: 'is_active', data: 'active'}, {name: 'is_content', data: 'content'} ]); }); }); document.querySelectorAll('.js-email-recipient').forEach(function (select) { select.addEventListener('change', function () { var form = select.closest('form'); if (!form || !select.selectedOptions[0]) return; var option = select.selectedOptions[0]; var email = form.querySelector('[name="to_email"]'); var company = form.querySelector('[name="company_name"]'); var clientId = form.querySelector('[name="client_id"]'); var logo = form.querySelector('[name="logo_url"]'); if (email) email.value = option.dataset.email || ''; if (company) company.value = option.dataset.company || ''; if (clientId) clientId.value = option.dataset.clientId || ''; if (logo) logo.value = option.dataset.logo || ''; updateEmailLogoPreview(form); }); }); function updateEmailLogoPreview(form) { if (!form) return; var input = form.querySelector('[name="logo_url"]'); var wrap = form.querySelector('.email-logo-preview-wrap'); var image = form.querySelector('.email-logo-preview'); if (!input || !wrap || !image) return; var value = input.value.trim(); if (value) { image.src = resolveAdminAssetUrl(value); wrap.classList.remove('d-none'); } else { image.removeAttribute('src'); wrap.classList.add('d-none'); } } document.querySelectorAll('form [name="logo_url"]').forEach(function (input) { input.addEventListener('input', function () { updateEmailLogoPreview(input.closest('form')); }); }); document.querySelectorAll('form [name="logo_file"]').forEach(function (input) { input.addEventListener('change', function () { var form = input.closest('form'); var wrap = form ? form.querySelector('.email-logo-preview-wrap') : null; var image = form ? form.querySelector('.email-logo-preview') : null; if (!wrap || !image || !input.files || !input.files[0]) return; image.src = URL.createObjectURL(input.files[0]); wrap.classList.remove('d-none'); }); }); // Edit item modal population document.querySelectorAll('.js-edit-item').forEach(function (button) { button.addEventListener('click', function () { var form = document.querySelector('#itemEditModal form'); fillFormFromDataset(form, button.dataset, [ {name: 'id', data: 'id'}, {name: 'order_id', data: 'orderId'}, {name: 'product_id', data: 'productId'}, {name: 'item_type', data: 'itemType'}, {name: 'title', data: 'title'}, {name: 'company', data: 'company'}, {name: 'genre', data: 'genre'}, {name: 'media_url', data: 'mediaUrl'}, {name: 'action_url', data: 'actionUrl'}, {name: 'sort_order', data: 'sortOrder'}, {name: 'is_active', data: 'active'}, {name: 'allow_download', data: 'allowDownload'} ]); // Handle cover image section visibility var orderSelect = form.querySelector('.js-order-select'); var coverSection = form.querySelector('.js-cover-image-section'); var coverPreview = form.querySelector('.js-cover-preview'); if (orderSelect && coverSection) { var isCatalog = !button.dataset.orderId || button.dataset.orderId === ''; coverSection.style.display = isCatalog ? 'block' : 'none'; // Load existing cover image if available if (isCatalog && button.dataset.coverImage && coverPreview) { coverPreview.innerHTML = '<img src="' + button.dataset.coverImage + '" style="width: 100%; height: 100%; object-fit: cover;">'; } } var product = form.querySelector('.js-content-product'); if (product && !product.value) { applyContentProductType(product); } }); }); // Add content modal reset document.querySelectorAll('.js-add-content').forEach(function (button) { button.addEventListener('click', function () { var form = document.querySelector('#itemCreateModal form'); if (!form) return; form.reset(); var order = form.querySelector('[name="order_id"]'); if (order) order.value = button.dataset.orderId || ''; var active = form.querySelector('[name="is_active"]'); if (active) active.checked = true; var download = form.querySelector('[name="allow_download"]'); if (download) download.checked = false; // Hide cover image section by default var coverSection = form.querySelector('.js-cover-image-section'); if (coverSection) coverSection.style.display = 'none'; }); }); // Handle order select change to show/hide cover image section document.querySelectorAll('#itemCreateModal .js-order-select, #itemEditModal .js-order-select').forEach(function (select) { select.addEventListener('change', function () { var form = select.closest('form'); var coverSection = form.querySelector('.js-cover-image-section'); var coverPreview = form.querySelector('.js-cover-preview'); if (coverSection) { var isCatalog = !select.value || select.value === ''; coverSection.style.display = isCatalog ? 'block' : 'none'; // Reset preview if switching to non-catalog if (!isCatalog && coverPreview) { coverPreview.innerHTML = '<span class="text-muted">Sin imagen</span>'; } } }); }); // Handle cover image file input preview document.querySelectorAll('input[name="cover_image_file"]').forEach(function (input) { input.addEventListener('change', function (e) { var form = input.closest('form'); var preview = form.querySelector('.js-cover-preview'); if (e.target.files && e.target.files[0] && preview) { var file = e.target.files[0]; // Validate file type if (!file.type.match('image.*')) { alert('Por favor selecciona un archivo de imagen válido'); input.value = ''; return; } var reader = new FileReader(); reader.onload = function (event) { preview.innerHTML = '<img src="' + event.target.result + '" style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px;">'; }; reader.onerror = function() { alert('Error al leer el archivo'); }; reader.readAsDataURL(file); } else if (preview && (!e.target.files || e.target.files.length === 0)) { preview.innerHTML = '<span class="text-muted">Sin imagen</span>'; } }); }); // Invoice editor functionality document.querySelectorAll('.invoice-editor').forEach(function (table) { var form = table.closest('form'); table.querySelectorAll('.js-toggle-discount').forEach(function (button) { button.addEventListener('click', function () { var row = button.closest('.js-invoice-row'); var select = row.querySelector('[name="invoice_discount_type[]"]'); var visible = !select || select.classList.contains('d-none'); setRowDiscountVisible(row, visible); calculateInvoice(form); }); }); var addButton = form.querySelector('.js-add-invoice-row'); if (addButton) { addButton.addEventListener('click', function () { var row = table.querySelector('.js-invoice-row.invoice-row-hidden'); if (!row) return; row.classList.remove('invoice-row-hidden'); var quantity = row.querySelector('[name="invoice_quantity[]"]'); if (quantity && !quantity.value) quantity.value = '1'; var product = row.querySelector('[name="invoice_product_id[]"]'); if (product) product.focus(); calculateInvoice(form); }); } table.querySelectorAll('.js-remove-invoice-row').forEach(function (button) { button.addEventListener('click', function () { var row = button.closest('.js-invoice-row'); if (!row) return; var visibleRows = visibleInvoiceRows(table); if (visibleRows.length <= 1) { clearInvoiceRow(row); } else { clearInvoiceRow(row); row.classList.add('invoice-row-hidden'); } calculateInvoice(form); }); }); table.addEventListener('input', function () { calculateInvoice(form); }); table.addEventListener('change', function (event) { if (event.target && event.target.name === 'invoice_quantity[]') { event.target.value = quantityValue(event.target); } if (event.target && event.target.name === 'invoice_product_id[]') { applyProductPrice(event.target, true); } calculateInvoice(form); }); if (form) { ['order_discount_type', 'order_discount', 'tax_rate'].forEach(function (name) { var input = form.querySelector('[name="' + name + '"]'); if (input) { input.addEventListener('input', function () { calculateInvoice(form); }); input.addEventListener('change', function () { calculateInvoice(form); }); } }); calculateInvoice(form); } }); // Content editor functionality document.querySelectorAll('.content-editor').forEach(function (table) { var form = table.closest('form'); var addButton = form.querySelector('.js-add-content-row'); if (addButton) { addButton.addEventListener('click', function () { var row = table.querySelector('.js-content-row.invoice-row-hidden'); if (!row) return; row.classList.remove('invoice-row-hidden'); var sort = row.querySelector('[name="content_sort_order[]"]'); if (sort && !sort.value) sort.value = visibleContentRows(table).length - 1; var active = row.querySelector('.js-content-active') || row.querySelector('input[type="checkbox"]'); if (active) active.checked = true; var download = row.querySelector('.js-content-download'); if (download) download.checked = false; var product = row.querySelector('[name="content_product_id[]"]'); if (product) product.focus(); }); } table.querySelectorAll('.js-remove-content-row').forEach(function (button) { button.addEventListener('click', function () { var row = button.closest('.js-content-row'); if (!row) return; var visibleRows = visibleContentRows(table); if (visibleRows.length <= 1) { clearContentRow(row); } else { clearContentRow(row); row.classList.add('invoice-row-hidden'); } }); }); table.addEventListener('change', function (event) { if (event.target && event.target.classList.contains('js-content-product')) { applyContentProductType(event.target); } }); }); document.querySelectorAll('#itemCreateModal .js-content-product, #itemEditModal .js-content-product').forEach(function (select) { select.addEventListener('change', function () { applyContentProductType(select); }); }); // Edit order modal population document.querySelectorAll('.js-edit-order').forEach(function (button) { button.addEventListener('click', function () { var form = document.querySelector('#orderEditModal form'); fillFormFromDataset(form, button.dataset, [ {name: 'id', data: 'id'}, {name: 'client_id', data: 'clientId'}, {name: 'order_contact_id', data: 'orderContactId'}, {name: 'title', data: 'title'}, {name: 'status', data: 'status'}, {name: 'notes', data: 'notes'}, {name: 'ordered_at', data: 'orderedAt'}, {name: 'script_delivery_date', data: 'scriptDeliveryDate'}, {name: 'final_delivery_date', data: 'finalDeliveryDate'}, {name: 'order_discount_type', data: 'orderDiscountType'}, {name: 'order_discount', data: 'orderDiscount'}, {name: 'tax_rate', data: 'taxRate'}, {name: 'terms_conditions', data: 'termsConditions'}, {name: 'client_signature', data: 'clientSignature'}, {name: 'marketing_objective', data: 'marketingObjective'}, {name: 'marketing_audience', data: 'marketingAudience'}, {name: 'marketing_channels', data: 'marketingChannels'}, {name: 'marketing_references', data: 'marketingReferences'}, {name: 'marketing_notes', data: 'marketingNotes'}, {name: 'marketing_purpose_1', data: 'marketingPurposeOne'}, {name: 'marketing_purpose_2', data: 'marketingPurposeTwo'}, {name: 'marketing_purpose_3', data: 'marketingPurposeThree'}, {name: 'marketing_location', data: 'marketingLocation'}, {name: 'marketing_gender', data: 'marketingGender'}, {name: 'marketing_ages', data: 'marketingAges'}, {name: 'marketing_socioeconomic', data: 'marketingSocioeconomic'}, {name: 'marketing_interests', data: 'marketingInterests'}, {name: 'marketing_message', data: 'marketingMessage'}, {name: 'marketing_voice_tone', data: 'marketingVoiceTone'}, {name: 'marketing_music_genres', data: 'marketingMusicGenres'}, {name: 'marketing_slogans', data: 'marketingSlogans'}, {name: 'marketing_promotions', data: 'marketingPromotions'}, {name: 'marketing_inspirations', data: 'marketingInspirations'}, {name: 'marketing_considerations', data: 'marketingConsiderations'} ]); try { fillInvoiceRows(form, JSON.parse(button.dataset.invoiceItems || '[]')); } catch (error) { fillInvoiceRows(form, []); } try { fillContentRows(form, JSON.parse(button.dataset.contentItems || '[]')); } catch (error) { fillContentRows(form, []); } syncMarketingChecklists(form); updatePurposeOptions(form); loadSignatureCanvases(form); // Populate contact dropdown for selected client var clientSelect = form.querySelector('.js-order-client-select'); if (clientSelect && clientSelect.value) { populateContactDropdown(form, clientSelect.value); } // Update signature label updateClientSignatureLabel(form); // Regenerate contract terms if dates are present // This ensures the contract shows current dates instead of placeholders var termsTextarea = form.querySelector('.js-order-terms'); if (termsTextarea && termsTextarea.value) { // Only regenerate if the contract contains placeholder text or old format if (termsTextarea.value.indexOf('[FECHA') !== -1 || termsTextarea.value.indexOf('al cabo de un día') !== -1 || termsTextarea.value.indexOf('Desde la aprobación de los textos') !== -1) { regenerateContractTerms(form); } } }); }); // Contact dropdown population based on client selection var contactsByClient = <?= json_encode($contactsByClient, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>; function populateContactDropdown(form, clientId) { var contactSelect = form.querySelector('.js-order-contact-select'); if (!contactSelect) return; var contacts = contactsByClient[clientId] || []; var currentValue = contactSelect.value; contactSelect.innerHTML = '<option value="">Seleccionar contacto</option>'; contacts.forEach(function(contact) { var option = document.createElement('option'); option.value = contact.id; option.textContent = contact.name; option.dataset.name = contact.name; contactSelect.appendChild(option); }); // If only one contact, select it automatically if (contacts.length === 1) { contactSelect.value = contacts[0].id; } else if (currentValue) { contactSelect.value = currentValue; } updateClientSignatureLabel(form); } function updateClientSignatureLabel(form) { var clientSelect = form.querySelector('.js-order-client-select'); var contactSelect = form.querySelector('.js-order-contact-select'); var label = form.querySelector('.js-client-signature-label'); if (!label) return; var clientName = ''; var contactName = ''; if (clientSelect && clientSelect.selectedOptions[0]) { clientName = clientSelect.selectedOptions[0].dataset.name || clientSelect.selectedOptions[0].textContent; } if (contactSelect && contactSelect.selectedOptions[0] && contactSelect.value) { contactName = contactSelect.selectedOptions[0].dataset.name || contactSelect.selectedOptions[0].textContent; } if (contactName && clientName) { label.innerHTML = '<strong>' + contactName + '</strong><br>' + clientName; } else if (clientName) { label.innerHTML = clientName; } else { label.innerHTML = ''; } } function replaceClientePlaceholder(form) { var clientSelect = form.querySelector('.js-order-client-select'); var termsTextarea = form.querySelector('.js-order-terms'); if (!clientSelect || !termsTextarea) return; // Regenerate the entire contract with updated client name and dates regenerateContractTerms(form); } // Listen for client selection changes document.querySelectorAll('.js-order-client-select').forEach(function(select) { select.addEventListener('change', function() { var form = select.closest('form'); populateContactDropdown(form, select.value); replaceClientePlaceholder(form); }); }); // Listen for contact selection changes document.querySelectorAll('.js-order-contact-select').forEach(function(select) { select.addEventListener('change', function() { var form = select.closest('form'); updateClientSignatureLabel(form); }); }); // Auto-calculate delivery dates based on order date function calculateDeliveryDates(orderDate) { if (!orderDate) return {scriptDate: '', finalDate: ''}; var date = new Date(orderDate + 'T00:00:00'); // Script delivery: 1 day after order date var scriptDate = new Date(date); scriptDate.setDate(scriptDate.getDate() + 1); // Final delivery: 2 days after order date var finalDate = new Date(date); finalDate.setDate(finalDate.getDate() + 2); return { scriptDate: scriptDate.toISOString().split('T')[0], finalDate: finalDate.toISOString().split('T')[0] }; } // Format date from YYYY-MM-DD to DD/MM/YYYY function formatDateDDMMYYYY(dateStr) { if (!dateStr) return '[FECHA]'; var parts = dateStr.split('-'); if (parts.length !== 3) return '[FECHA]'; return parts[2] + '/' + parts[1] + '/' + parts[0]; } // Regenerate contract terms with current form data function regenerateContractTerms(form) { var termsTextarea = form.querySelector('.js-order-terms'); if (!termsTextarea) return; var clientSelect = form.querySelector('.js-order-client-select'); var scriptDateInput = form.querySelector('.js-script-delivery-date'); var finalDateInput = form.querySelector('.js-final-delivery-date'); var clientName = '[CLIENTE]'; if (clientSelect && clientSelect.selectedOptions[0]) { clientName = clientSelect.selectedOptions[0].dataset.name || clientSelect.selectedOptions[0].textContent; } var scriptDate = scriptDateInput ? scriptDateInput.value : ''; var finalDate = finalDateInput ? finalDateInput.value : ''; var scriptDateFormatted = formatDateDDMMYYYY(scriptDate); var finalDateFormatted = formatDateDDMMYYYY(finalDate); // Generate the contract terms with the new format var terms = "1. Objeto del Contrato\neMprendo se compromete a crear un paquete de contenidos según lo incluido en esta factura:\n\n"; terms += "2. Plazo de Entrega\nEl tiempo de entrega será:\n"; terms += "1️⃣ Guiones e ideas: " + scriptDateFormatted + " para la revisión inmediata del cliente " + clientName + ".\n"; terms += "2️⃣ Entrega Final: " + finalDateFormatted + ". Se considerará un tiempo prudencial extra en caso de correcciones posteriores a la entrega.\n\n"; terms += "3. Condiciones de Pago\nEl cliente " + clientName + " se compromete a pagar el 50% del costo total al inicio del trabajo, y el 50% restante al momento de la entrega del trabajo, en el tiempo y según los métodos de pago estipulados en la presente factura.\n\n"; terms += "4. Revisión de Contenidos\nEl cliente " + clientName + " tiene derecho a:\n"; terms += "1️⃣ Una revisión de los guiones a ser utilizados en los spots y las canciones antes de su realización.\n"; terms += "2️⃣ Una corrección adicional del producto creado tras la aprobación de los textos.\n"; terms += "Cualquier corrección adicional a las dos mencionadas será cobrada adicionalmente.\n\n"; terms += "5. Responsabilidades del Cliente\nEl cliente " + clientName + " se compromete a brindar toda la información necesaria para la realización de los contenidos.\n\n"; terms += "6. Análisis de Mercado\neMprendo se compromete a realizar un análisis de mercado a través de la herramienta preparada para este fin.\n\n"; terms += "7. Derechos de Autor\nSe ceden los derechos de explotación de las obras al cliente, y la empresa se reserva los derechos de creación conforme a lo establecido en las políticas de derechos de autor internacional.\n\n"; terms += "8. Confidencialidad\neMprendo se compromete a mantener la confidencialidad de los datos que aporte el cliente " + clientName + " y a no divulgar dicha información a terceros sin el consentimiento previo.\n\n"; terms += "9. Modificaciones del Contrato\nCualquier modificación a este contrato deberá ser consentida por ambas partes y documentada por escrito.\n\n"; terms += "10. Firma Electrónica\nLas firmas electrónicas que se realicen para dar validez a este contrato tendrán la misma validez legal que las firmas manuscritas.\n\n"; terms += "11. Fuerza Mayor\nNinguna de las partes será responsable por el incumplimiento de sus obligaciones bajo este contrato si dicho incumplimiento es causado por eventos de fuerza mayor, tales como desastres naturales, actos de gobierno, guerras, o cualquier otra causa fuera del control razonable de las partes.\n\n"; terms += "12. Aceptación del Contrato\nAl firmar este contrato, ambas partes aceptan los términos y condiciones aquí establecidos."; termsTextarea.value = terms; } // Listen for order date changes to auto-calculate delivery dates and regenerate contract document.querySelectorAll('.js-order-date').forEach(function(input) { input.addEventListener('change', function() { var form = input.closest('form'); var scriptInput = form.querySelector('.js-script-delivery-date'); var finalInput = form.querySelector('.js-final-delivery-date'); if (!scriptInput || !finalInput) return; var dates = calculateDeliveryDates(input.value); scriptInput.value = dates.scriptDate; finalInput.value = dates.finalDate; // Regenerate contract terms with new dates regenerateContractTerms(form); }); // Auto-calculate on page load if order date is set but delivery dates are empty var form = input.closest('form'); var scriptInput = form.querySelector('.js-script-delivery-date'); var finalInput = form.querySelector('.js-final-delivery-date'); if (input.value && scriptInput && finalInput && !scriptInput.value && !finalInput.value) { var dates = calculateDeliveryDates(input.value); scriptInput.value = dates.scriptDate; finalInput.value = dates.finalDate; } }); // Listen for delivery date changes to regenerate contract document.querySelectorAll('.js-script-delivery-date, .js-final-delivery-date').forEach(function(input) { input.addEventListener('change', function() { var form = input.closest('form'); regenerateContractTerms(form); }); }); // Edit user modal population document.querySelectorAll('.js-edit-user').forEach(function (button) { button.addEventListener('click', function () { var form = document.querySelector('#userEditModal form'); fillFormFromDataset(form, button.dataset, [ {name: 'id', data: 'id'}, {name: 'name', data: 'name'}, {name: 'username', data: 'username'}, {name: 'signature_url', data: 'signatureUrl'}, {name: 'photo_url', data: 'photoUrl'}, {name: 'is_active', data: 'active'} ]); var password = form.querySelector('[name="password"]'); if (password) password.value = ''; updatePhotoPreview(form); loadSignatureCanvases(form); }); }); document.querySelectorAll('form [name="photo_url"]').forEach(function (input) { input.addEventListener('input', function () { updatePhotoPreview(input.closest('form')); }); }); document.querySelectorAll('form [name="photo_file"]').forEach(function (input) { input.addEventListener('change', function () { var form = input.closest('form'); var wrap = form ? form.querySelector('.profile-photo-preview-wrap') : null; var image = form ? form.querySelector('.profile-photo-preview') : null; if (!wrap || !image || !input.files || !input.files[0]) return; image.src = URL.createObjectURL(input.files[0]); wrap.classList.remove('d-none'); }); }); var profileEditModal = document.getElementById('profileEditModal'); if (profileEditModal) { profileEditModal.addEventListener('shown.bs.modal', function () { var form = profileEditModal.querySelector('form'); updatePhotoPreview(form); loadSignatureCanvases(form); }); } // Load signatures when order create modal is shown var orderCreateModal = document.getElementById('orderCreateModal'); if (orderCreateModal) { orderCreateModal.addEventListener('shown.bs.modal', function () { var form = orderCreateModal.querySelector('form'); if (form) { loadSignatureCanvases(form); } }); } // Toggle password visibility document.querySelectorAll('.js-toggle-password').forEach(function (button) { button.addEventListener('click', function () { var input = button.closest('.input-group').querySelector('input'); var icon = button.querySelector('i'); if (!input) return; var visible = input.type === 'text'; input.type = visible ? 'password' : 'text'; if (icon) { icon.classList.toggle('fa-eye', visible); icon.classList.toggle('fa-eye-slash', !visible); } button.setAttribute('aria-label', visible ? 'Ver clave' : 'Ocultar clave'); }); }); // Inline content title editing with AJAX document.querySelectorAll('.js-inline-content-title').forEach(function (titleNode) { titleNode.addEventListener('click', function () { if (titleNode.dataset.editing === '1') return; titleNode.dataset.editing = '1'; var previous = titleNode.textContent.trim(); var input = document.createElement('input'); input.className = 'form-control form-control-sm'; input.value = previous; titleNode.replaceWith(input); input.focus(); input.select(); var save = function () { var next = input.value.trim() || previous; var body = new FormData(); body.append('csrf_token', '<?= e(csrf_token()) ?>'); body.append('action', 'update_content_title'); body.append('id', titleNode.dataset.id || ''); body.append('title', next); fetch('index.php', {method: 'POST', body: body, headers: {'X-Requested-With': 'XMLHttpRequest'}}).finally(function () { titleNode.textContent = next; titleNode.dataset.editing = ''; input.replaceWith(titleNode); }); }; input.addEventListener('blur', save, {once: true}); input.addEventListener('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); input.blur(); } if (event.key === 'Escape') { input.value = previous; input.blur(); } }); }); }); // Audio player functionality function formatPlayerTime(seconds) { seconds = Math.max(0, Math.floor(seconds || 0)); var minutes = Math.floor(seconds / 60); return minutes + ':' + String(seconds % 60).padStart(2, '0'); } function setAudioButtonState(button, playing) { if (!button) return; var icon = button.querySelector('i'); if (!icon) return; icon.classList.toggle('fa-play', !playing); icon.classList.toggle('fa-pause', playing); } var adminAudio = new Audio(); var activeAudioButton = null; var audioPlayer = document.getElementById('adminAudioPlayer'); var audioTitle = document.getElementById('adminAudioTitle'); var audioToggle = document.getElementById('adminAudioToggle'); var audioClose = document.getElementById('adminAudioClose'); var audioProgress = document.getElementById('adminAudioProgress'); var audioCurrent = document.getElementById('adminAudioCurrent'); var audioDuration = document.getElementById('adminAudioDuration'); function syncFloatingPlayer() { if (!audioPlayer) return; var duration = adminAudio.duration || 0; var current = adminAudio.currentTime || 0; if (audioProgress) { audioProgress.value = duration ? String((current / duration) * 100) : '0'; } if (audioCurrent) audioCurrent.textContent = formatPlayerTime(current); if (audioDuration) audioDuration.textContent = formatPlayerTime(duration); if (audioToggle) { var icon = audioToggle.querySelector('i'); if (icon) { icon.classList.toggle('fa-play', adminAudio.paused); icon.classList.toggle('fa-pause', !adminAudio.paused); } audioToggle.setAttribute('aria-label', adminAudio.paused ? 'Reproducir' : 'Pausar'); } setAudioButtonState(activeAudioButton, !adminAudio.paused); } document.querySelectorAll('.js-audio-preview').forEach(function (button) { button.addEventListener('click', function () { var src = button.dataset.src || ''; if (!src) return; if (activeAudioButton === button && !adminAudio.paused) { adminAudio.pause(); syncFloatingPlayer(); return; } setAudioButtonState(activeAudioButton, false); activeAudioButton = button; if (adminAudio.src !== src) { adminAudio.src = src; if (audioProgress) audioProgress.value = '0'; } if (audioTitle) audioTitle.textContent = button.dataset.title || 'Audio'; if (audioPlayer) audioPlayer.classList.add('is-visible'); adminAudio.play(); syncFloatingPlayer(); }); }); if (audioToggle) { audioToggle.addEventListener('click', function () { if (!adminAudio.src) return; if (adminAudio.paused) { adminAudio.play(); } else { adminAudio.pause(); } syncFloatingPlayer(); }); } if (audioClose) { audioClose.addEventListener('click', function () { adminAudio.pause(); adminAudio.removeAttribute('src'); adminAudio.load(); setAudioButtonState(activeAudioButton, false); activeAudioButton = null; if (audioPlayer) audioPlayer.classList.remove('is-visible'); if (audioProgress) audioProgress.value = '0'; if (audioCurrent) audioCurrent.textContent = '0:00'; if (audioDuration) audioDuration.textContent = '0:00'; }); } if (audioProgress) { audioProgress.addEventListener('input', function () { if (!adminAudio.duration) return; adminAudio.currentTime = adminAudio.duration * (parseFloat(audioProgress.value) || 0) / 100; syncFloatingPlayer(); }); } adminAudio.addEventListener('timeupdate', syncFloatingPlayer); adminAudio.addEventListener('loadedmetadata', syncFloatingPlayer); adminAudio.addEventListener('pause', syncFloatingPlayer); adminAudio.addEventListener('play', syncFloatingPlayer); adminAudio.addEventListener('ended', function () { setAudioButtonState(activeAudioButton, false); syncFloatingPlayer(); }); // Sector "Otro" field toggle document.querySelectorAll('.js-sector-select').forEach(function (select) { var otherField = select.parentElement.querySelector('.js-sector-other'); if (!otherField) return; function toggleOtherField() { var isOther = select.value === 'Otro'; otherField.classList.toggle('d-none', !isOther); if (isOther) { otherField.required = true; otherField.focus(); // Scroll to the "Otro" field within the modal setTimeout(function() { var modalBody = otherField.closest('.modal-body'); if (modalBody) { var fieldPosition = otherField.offsetTop; var modalBodyHeight = modalBody.clientHeight; var scrollPosition = fieldPosition - (modalBodyHeight / 2) + 50; modalBody.scrollTo({ top: scrollPosition, behavior: 'smooth' }); } }, 100); } else { otherField.required = false; otherField.value = ''; } } select.addEventListener('change', toggleOtherField); toggleOtherField(); // Initialize on page load }); // Video preview modal var videoModal = document.getElementById('videoPreviewModal'); if (videoModal) { var videoElement = videoModal.querySelector('video'); var videoTitle = videoModal.querySelector('.modal-title'); videoModal.addEventListener('show.bs.modal', function (event) { var trigger = event.relatedTarget; if (!trigger || !videoElement) return; videoModal.classList.remove('is-vertical'); videoElement.src = trigger.dataset.src || ''; if (videoTitle) videoTitle.textContent = trigger.dataset.title || 'Vista previa de video'; }); if (videoElement) { videoElement.addEventListener('loadedmetadata', function () { if (videoElement.videoHeight > videoElement.videoWidth) { videoModal.classList.add('is-vertical'); } }); } videoModal.addEventListener('hidden.bs.modal', function () { if (videoElement) { videoElement.pause(); videoElement.src = ''; } }); } </script> <!-- Terms editor with formatting --> <script> // Format terms text with bold HTML tags function formatTermsHTML(text, clientName) { if (!text) return ''; let formatted = text; // Escape HTML first formatted = formatted.replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); // Bold clause titles (1. Title, 2. Title, etc.) formatted = formatted.replace(/^(\d+\.\s+[^\n]+)$/gm, '<strong>$1</strong>'); // Bold eMprendo formatted = formatted.replace(/eMprendo/g, '<strong>eMprendo</strong>'); // Bold client name if provided if (clientName && clientName.trim()) { const escapedClientName = clientName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(escapedClientName, 'g'); formatted = formatted.replace(regex, '<strong>' + clientName + '</strong>'); } // Convert line breaks to <br> formatted = formatted.replace(/\n/g, '<br>'); return formatted; } // Strip HTML tags to get plain text function stripHTML(html) { const temp = document.createElement('div'); temp.innerHTML = html; return temp.textContent || temp.innerText || ''; } // Initialize terms editors document.querySelectorAll('.terms-editor').forEach(function(editor) { const container = editor.closest('.col-12'); const textarea = container.querySelector('.js-order-terms'); const form = editor.closest('form'); // Load initial content with formatting if (textarea.value) { // Get client name const clientSelect = form.querySelector('.js-order-client-select'); let clientName = ''; if (clientSelect && clientSelect.selectedOptions[0]) { clientName = clientSelect.selectedOptions[0].dataset.name || ''; } editor.innerHTML = formatTermsHTML(textarea.value, clientName); } // Sync editor content to hidden textarea on input editor.addEventListener('input', function() { // Get plain text from editor (strip HTML) const plainText = editor.innerText || editor.textContent || ''; textarea.value = plainText; }); // Sync editor content to hidden textarea on blur editor.addEventListener('blur', function() { const plainText = editor.innerText || editor.textContent || ''; textarea.value = plainText; }); // Re-format when client changes const clientSelect = form.querySelector('.js-order-client-select'); if (clientSelect) { clientSelect.addEventListener('change', function() { const clientName = clientSelect.selectedOptions[0] ? (clientSelect.selectedOptions[0].dataset.name || '') : ''; const plainText = editor.innerText || editor.textContent || ''; editor.innerHTML = formatTermsHTML(plainText, clientName); }); } // Sync before form submission form.addEventListener('submit', function() { const plainText = editor.innerText || editor.textContent || ''; textarea.value = plainText; }); }); // Update terms editor when terms are regenerated const originalRegenerateContractTerms = window.regenerateContractTerms; if (typeof originalRegenerateContractTerms === 'function') { window.regenerateContractTerms = function(form) { originalRegenerateContractTerms(form); // Update the editor with formatted content const container = form.querySelector('.terms-editor'); if (container) { const textarea = form.querySelector('.js-order-terms'); const clientSelect = form.querySelector('.js-order-client-select'); let clientName = ''; if (clientSelect && clientSelect.selectedOptions[0]) { clientName = clientSelect.selectedOptions[0].dataset.name || ''; } if (textarea && textarea.value) { container.innerHTML = formatTermsHTML(textarea.value, clientName); } } }; } </script> <!-- Table sorting functionality --> <script> document.querySelectorAll('.js-sortable-table').forEach(function(table) { var headers = table.querySelectorAll('.js-sortable'); var tbody = table.querySelector('tbody'); var currentSort = { column: null, direction: 'asc' }; // Helper function to parse order codes like 260313-0044 function parseOrderCode(code) { if (!code || typeof code !== 'string') return null; var match = code.match(/^(\d{6})-(\d+)$/); if (match) { return { date: parseInt(match[1]), number: parseInt(match[2]) }; } return null; } // Helper function to compare order codes function compareOrderCodes(a, b) { var parsedA = parseOrderCode(a); var parsedB = parseOrderCode(b); // If both are valid order codes, compare them properly if (parsedA && parsedB) { if (parsedA.date !== parsedB.date) { return parsedA.date - parsedB.date; } return parsedA.number - parsedB.number; } // If only one is valid, valid one comes first if (parsedA) return -1; if (parsedB) return 1; // Neither is valid, use string comparison return a.localeCompare(b, 'es'); } // Function to perform sort function performSort(sortKey, direction) { var rows = Array.from(tbody.querySelectorAll('tr[data-filter-row]')); rows.sort(function(a, b) { var aVal = a.dataset['sort' + sortKey.charAt(0).toUpperCase() + sortKey.slice(1)] || ''; var bVal = b.dataset['sort' + sortKey.charAt(0).toUpperCase() + sortKey.slice(1)] || ''; var result; // Special handling for 'title' column (order codes) if (sortKey === 'title') { result = compareOrderCodes(aVal, bVal); } else { // Try numeric comparison first var aNum = parseFloat(aVal); var bNum = parseFloat(bVal); if (!isNaN(aNum) && !isNaN(bNum)) { result = aNum - bNum; } else { // String comparison result = aVal.localeCompare(bVal, 'es'); } } return direction === 'asc' ? result : -result; }); // Reorder rows in DOM (including their collapse rows) rows.forEach(function(row) { var collapseRow = row.nextElementSibling; tbody.appendChild(row); if (collapseRow && collapseRow.classList.contains('collapse-row')) { tbody.appendChild(collapseRow); } }); } // Function to update icon function updateIcon(header, direction) { var icon = header.querySelector('i'); if (icon) { icon.className = direction === 'asc' ? 'fa-solid fa-sort-up text-primary' : 'fa-solid fa-sort-down text-primary'; icon.style.opacity = '1'; } } headers.forEach(function(header) { header.style.cursor = 'pointer'; header.style.userSelect = 'none'; header.addEventListener('click', function() { var sortKey = header.dataset.sort; // Toggle direction if same column, otherwise reset to asc if (currentSort.column === sortKey) { currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; } else { currentSort.direction = 'asc'; } currentSort.column = sortKey; // Update all icons headers.forEach(function(h) { var i = h.querySelector('i'); if (i) { i.className = 'fa-solid fa-sort text-muted'; i.style.fontSize = '0.75rem'; i.style.opacity = '0.4'; } }); // Update current icon updateIcon(header, currentSort.direction); // Perform sort performSort(sortKey, currentSort.direction); }); }); // Auto-sort by 'title' (Pedido) column descending on page load for orders table if (table.id === 'ordersTable') { var titleHeader = Array.from(headers).find(function(h) { return h.dataset.sort === 'title'; }); if (titleHeader) { currentSort.column = 'title'; currentSort.direction = 'desc'; // Update all icons to default headers.forEach(function(h) { var i = h.querySelector('i'); if (i) { i.className = 'fa-solid fa-sort text-muted'; i.style.fontSize = '0.75rem'; i.style.opacity = '0.4'; } }); // Update title header icon updateIcon(titleHeader, 'desc'); // Perform initial sort performSort('title', 'desc'); } } }); </script> <!-- Mobile Navigation JavaScript --> <script> (function() { // Mobile menu toggle var mobileMenuBtn = document.getElementById('mobileMenuBtn'); var mobileSidebar = document.getElementById('mobileSidebar'); var mobileOverlay = document.getElementById('mobileOverlay'); if (mobileMenuBtn && mobileSidebar && mobileOverlay) { mobileMenuBtn.addEventListener('click', function() { mobileSidebar.classList.add('open'); mobileOverlay.classList.add('show'); document.body.style.overflow = 'hidden'; }); mobileOverlay.addEventListener('click', function() { mobileSidebar.classList.remove('open'); mobileOverlay.classList.remove('show'); document.body.style.overflow = ''; }); // Close sidebar when clicking a link var sidebarLinks = mobileSidebar.querySelectorAll('.mobile-sidebar-item'); sidebarLinks.forEach(function(link) { link.addEventListener('click', function() { setTimeout(function() { mobileSidebar.classList.remove('open'); mobileOverlay.classList.remove('show'); document.body.style.overflow = ''; }, 200); }); }); } // Floating Action Button var fabBtn = document.getElementById('fabBtn'); if (fabBtn) { fabBtn.addEventListener('click', function() { var section = '<?= $currentSection ?>'; var modalMap = { 'clients': '#clientCreateModal', 'orders': '#orderCreateModal', 'contents': '#itemCreateModal', 'products': '#productCreateModal', 'users': '#userCreateModal' }; var modalId = modalMap[section]; if (modalId) { var modal = document.querySelector(modalId); if (modal) { var bsModal = new bootstrap.Modal(modal); bsModal.show(); } } }); } // Smooth scroll to top on section change if (window.performance && window.performance.navigation.type === window.performance.navigation.TYPE_NAVIGATE) { window.scrollTo(0, 0); } // Add touch feedback to mobile nav items var mobileNavItems = document.querySelectorAll('.mobile-nav-item'); mobileNavItems.forEach(function(item) { item.addEventListener('touchstart', function() { this.style.opacity = '0.6'; }); item.addEventListener('touchend', function() { this.style.opacity = '1'; }); }); // Prevent pull-to-refresh on iOS when scrolling at top var lastTouchY = 0; var preventPullToRefresh = false; document.addEventListener('touchstart', function(e) { if (e.touches.length !== 1) return; lastTouchY = e.touches[0].clientY; preventPullToRefresh = window.pageYOffset === 0; }, { passive: false }); document.addEventListener('touchmove', function(e) { var touchY = e.touches[0].clientY; var touchYDelta = touchY - lastTouchY; lastTouchY = touchY; if (preventPullToRefresh && touchYDelta > 0) { e.preventDefault(); return; } }, { passive: false }); })(); </script> <!-- Hash redirect for backward compatibility --> <script> if (window.location.hash && !window.location.search) { const hashMap = { '#clientes-lista': '?section=clients', '#pedidos-admin': '?section=orders', '#contenidos-admin': '?section=contents', '#productos-admin': '?section=products', '#web-admin': '?section=web', '#usuarios-admin': '?section=users', }; const newUrl = hashMap[window.location.hash]; if (newUrl) { window.location.replace(newUrl + window.location.hash); } } </script> </body> </html>
Coded With 💗 by
0x6ick