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: profit_reports.php
<?php include '../components/connect.php'; session_start(); $admin_id = $_SESSION['admin_id'] ?? null; if(!$admin_id){ header('location:admin_login.php'); exit(); } date_default_timezone_set('America/Bogota'); $tz = new DateTimeZone('America/Bogota'); $validRanges = ['today','week','month','quarter','year','custom']; $range = $_GET['range'] ?? 'month'; if(!in_array($range, $validRanges, true)){ $range = 'month'; } $today = new DateTime('today', $tz); $start = clone $today; $end = clone $today; $rangeLabel = ''; $customStartParam = $_GET['start_date'] ?? ''; $customEndParam = $_GET['end_date'] ?? ''; switch($range){ case 'today': $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Hoy'; break; case 'week': $start = (clone $today)->modify('monday this week'); $end = (clone $start)->modify('sunday this week'); $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Semana actual'; break; case 'quarter': $month = (int)$today->format('n'); $quarterStartMonth = $month - (($month - 1) % 3); $start = new DateTime($today->format('Y') . '-' . str_pad((string)$quarterStartMonth, 2, '0', STR_PAD_LEFT) . '-01', $tz); $end = (clone $start)->modify('+2 months')->modify('last day of this month'); $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Trimestre actual'; break; case 'year': $start = new DateTime($today->format('Y') . '-01-01', $tz); $end = new DateTime($today->format('Y') . '-12-31', $tz); $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Año en curso'; break; case 'custom': $sanitizedStart = preg_match('/^\d{4}-\d{2}-\d{2}$/', $customStartParam) ? $customStartParam : ''; $sanitizedEnd = preg_match('/^\d{4}-\d{2}-\d{2}$/', $customEndParam) ? $customEndParam : ''; if($sanitizedStart === '' || $sanitizedEnd === ''){ $range = 'month'; $start = new DateTime($today->format('Y-m-01'), $tz); $end = (clone $start)->modify('last day of this month'); $rangeLabel = 'Mes actual'; } else { $start = DateTime::createFromFormat('Y-m-d', $sanitizedStart, $tz) ?: new DateTime($sanitizedStart, $tz); $end = DateTime::createFromFormat('Y-m-d', $sanitizedEnd, $tz) ?: new DateTime($sanitizedEnd, $tz); if($start > $end){ [$start, $end] = [$end, $start]; } $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Rango personalizado'; } break; case 'month': default: $start = new DateTime($today->format('Y-m-01'), $tz); $end = (clone $start)->modify('last day of this month'); $start->setTime(0,0,0); $end->setTime(23,59,59); $rangeLabel = 'Mes actual'; break; } $periodStart = $start->format('Y-m-d H:i:s'); $periodEnd = $end->format('Y-m-d H:i:s'); $summaryDefaults = ['revenue' => 0.0, 'cost' => 0.0, 'orders' => 0, 'items' => 0]; $dineStmt = $conn->prepare("SELECT COALESCE(SUM(doi.unit_price * doi.quantity), 0) AS revenue, COALESCE(SUM(doi.cost_price * doi.quantity), 0) AS cost, COUNT(DISTINCT dio.id) AS orders, COALESCE(SUM(doi.quantity), 0) AS items FROM dine_in_orders dio JOIN dine_in_order_items doi ON doi.order_id = dio.id WHERE dio.payment_status = 'pagada' AND dio.order_status != 'cancelada' AND COALESCE(dio.delivered_at, dio.updated_at, dio.created_at) BETWEEN ? AND ?"); $dineStmt->execute([$periodStart, $periodEnd]); $dineSummary = $dineStmt->fetch(PDO::FETCH_ASSOC) ?: $summaryDefaults; $dineSummary = array_merge($summaryDefaults, array_map('floatval', $dineSummary)); $dineSummary['orders'] = (int)($dineSummary['orders'] ?? 0); $dineSummary['items'] = (int)($dineSummary['items'] ?? 0); $deliveryStmt = $conn->prepare("SELECT COALESCE(SUM(doi.price * doi.quantity), 0) AS revenue, COALESCE(SUM(doi.cost_price * doi.quantity), 0) AS cost, COUNT(DISTINCT del.id) AS orders, COALESCE(SUM(doi.quantity), 0) AS items FROM delivery_orders del JOIN delivery_order_items doi ON doi.order_id = del.id WHERE del.status = 'entregado' AND COALESCE(del.updated_at, del.created_at) BETWEEN ? AND ?"); $deliveryStmt->execute([$periodStart, $periodEnd]); $deliverySummary = $deliveryStmt->fetch(PDO::FETCH_ASSOC) ?: $summaryDefaults; $deliverySummary = array_merge($summaryDefaults, array_map('floatval', $deliverySummary)); $deliverySummary['orders'] = (int)($deliverySummary['orders'] ?? 0); $deliverySummary['items'] = (int)($deliverySummary['items'] ?? 0); $totalRevenue = $dineSummary['revenue'] + $deliverySummary['revenue']; $totalCost = $dineSummary['cost'] + $deliverySummary['cost']; $totalProfit = $totalRevenue - $totalCost; $totalOrders = $dineSummary['orders'] + $deliverySummary['orders']; $totalItems = $dineSummary['items'] + $deliverySummary['items']; $profitMargin = $totalRevenue > 0 ? ($totalProfit / $totalRevenue) * 100 : 0; $avgTicket = $totalOrders > 0 ? $totalRevenue / $totalOrders : 0; $dineProfit = $dineSummary['revenue'] - $dineSummary['cost']; $deliveryProfit = $deliverySummary['revenue'] - $deliverySummary['cost']; $dineMargin = $dineSummary['revenue'] > 0 ? ($dineProfit / $dineSummary['revenue']) * 100 : 0; $deliveryMargin = $deliverySummary['revenue'] > 0 ? ($deliveryProfit / $deliverySummary['revenue']) * 100 : 0; $topDineStmt = $conn->prepare("SELECT doi.product_name AS name, SUM(doi.quantity) AS qty, SUM(doi.unit_price * doi.quantity) AS revenue, SUM(doi.cost_price * doi.quantity) AS cost FROM dine_in_orders dio JOIN dine_in_order_items doi ON doi.order_id = dio.id WHERE dio.payment_status = 'pagada' AND dio.order_status != 'cancelada' AND COALESCE(dio.delivered_at, dio.updated_at, dio.created_at) BETWEEN ? AND ? GROUP BY doi.product_name ORDER BY (SUM(doi.unit_price * doi.quantity) - SUM(doi.cost_price * doi.quantity)) DESC LIMIT 5"); $topDineStmt->execute([$periodStart, $periodEnd]); $topDineProducts = $topDineStmt->fetchAll(PDO::FETCH_ASSOC); $topDeliveryStmt = $conn->prepare("SELECT doi.product_name AS name, SUM(doi.quantity) AS qty, SUM(doi.price * doi.quantity) AS revenue, SUM(doi.cost_price * doi.quantity) AS cost FROM delivery_orders del JOIN delivery_order_items doi ON doi.order_id = del.id WHERE del.status = 'entregado' AND COALESCE(del.updated_at, del.created_at) BETWEEN ? AND ? GROUP BY doi.product_name ORDER BY (SUM(doi.price * doi.quantity) - SUM(doi.cost_price * doi.quantity)) DESC LIMIT 5"); $topDeliveryStmt->execute([$periodStart, $periodEnd]); $topDeliveryProducts = $topDeliveryStmt->fetchAll(PDO::FETCH_ASSOC); $inventoryStmt = $conn->query( "SELECT i.id, i.name, i.unit, i.stock_min, i.is_active, COALESCE(SUM(CASE m.movement_type WHEN 'in' THEN m.quantity WHEN 'out' THEN -m.quantity ELSE m.quantity END), 0) AS stock FROM `ingredients` i LEFT JOIN `inventory_movements` m ON m.ingredient_id = i.id WHERE i.is_active = 1 GROUP BY i.id ORDER BY i.name ASC" ); $inventoryItems = $inventoryStmt ? $inventoryStmt->fetchAll(PDO::FETCH_ASSOC) : []; $trendStmt = $conn->prepare("SELECT day, SUM(revenue) AS revenue, SUM(cost) AS cost FROM ( SELECT DATE(COALESCE(dio.delivered_at, dio.updated_at, dio.created_at)) AS day, SUM(doi.unit_price * doi.quantity) AS revenue, SUM(doi.cost_price * doi.quantity) AS cost FROM dine_in_orders dio JOIN dine_in_order_items doi ON doi.order_id = dio.id WHERE dio.payment_status = 'pagada' AND dio.order_status != 'cancelada' AND COALESCE(dio.delivered_at, dio.updated_at, dio.created_at) BETWEEN ? AND ? GROUP BY day UNION ALL SELECT DATE(COALESCE(del.updated_at, del.created_at)) AS day, SUM(doi.price * doi.quantity) AS revenue, SUM(doi.cost_price * doi.quantity) AS cost FROM delivery_orders del JOIN delivery_order_items doi ON doi.order_id = del.id WHERE del.status = 'entregado' AND COALESCE(del.updated_at, del.created_at) BETWEEN ? AND ? GROUP BY day ) combined GROUP BY day ORDER BY day ASC"); $trendStmt->execute([$periodStart, $periodEnd, $periodStart, $periodEnd]); $trendRows = $trendStmt->fetchAll(PDO::FETCH_ASSOC); $trendMap = []; foreach($trendRows as $row){ $dayKey = $row['day']; $trendMap[$dayKey] = [ 'revenue' => (float)$row['revenue'], 'cost' => (float)$row['cost'], ]; } $chartLabels = []; $chartRevenue = []; $chartCost = []; $chartProfit = []; $dailyRows = []; $cursor = clone $start; while($cursor <= $end){ $dayKey = $cursor->format('Y-m-d'); $displayLabel = $cursor->format('d M'); $chartLabels[] = $displayLabel; $revenueValue = $trendMap[$dayKey]['revenue'] ?? 0.0; $costValue = $trendMap[$dayKey]['cost'] ?? 0.0; $roundedRevenue = round($revenueValue, 2); $roundedCost = round($costValue, 2); $roundedProfit = round($revenueValue - $costValue, 2); $chartRevenue[] = $roundedRevenue; $chartCost[] = $roundedCost; $chartProfit[] = $roundedProfit; if (abs($roundedRevenue) > 0.0001 || abs($roundedCost) > 0.0001) { $dailyRows[] = [ 'date' => $cursor->format('d/m/Y'), 'revenue' => $roundedRevenue, 'cost' => $roundedCost, 'profit' => $roundedProfit, ]; } $cursor->modify('+1 day'); } $chartLabelsJson = json_encode($chartLabels, JSON_UNESCAPED_UNICODE); $chartRevenueJson = json_encode($chartRevenue, JSON_NUMERIC_CHECK); $chartCostJson = json_encode($chartCost, JSON_NUMERIC_CHECK); $chartProfitJson = json_encode($chartProfit, JSON_NUMERIC_CHECK); $rangeDescription = $start->format('d \d\e M \d\e Y') . ' - ' . $end->format('d \d\e M \d\e Y'); $formStartValue = $start->format('Y-m-d'); $formEndValue = $end->format('Y-m-d'); $quickRanges = [ 'today' => ['label' => 'Hoy', 'icon' => 'fa-sun'], 'week' => ['label' => 'Semana', 'icon' => 'fa-calendar-week'], 'month' => ['label' => 'Mes', 'icon' => 'fa-calendar-alt'], 'quarter' => ['label' => 'Trimestre', 'icon' => 'fa-calendar'], 'year' => ['label' => 'Año', 'icon' => 'fa-chart-line'], ]; $totalProfitPercent = $totalRevenue > 0 ? number_format($profitMargin, 2, ',', '.') . '%' : '0%'; $businessName = getBusinessName($conn); $businessLogo = getBusinessLogo($conn); $businessLogoVersion = getBusinessLogoVersion($conn); $businessPhone = getBusinessPhone($conn); $iconHref = '../icon.php?size=64' . ($businessLogoVersion !== '' ? '&v=' . rawurlencode($businessLogoVersion) : ''); $logoHref = '../' . ltrim((string)$businessLogo, '/'); if ($businessLogoVersion !== '') { $logoHref .= (strpos($logoHref, '?') === false ? '?' : '&') . 'v=' . rawurlencode($businessLogoVersion); } $pdfIssuedAt = (new DateTime('now', $tz))->format('d/m/Y H:i'); $pdfFileSafeName = preg_replace('/[^a-zA-Z0-9\-_]+/', '-', strtolower($businessName)); $pdfFilename = trim($pdfFileSafeName, '-') !== '' ? ('reporte-ganancias-' . trim($pdfFileSafeName, '-') . '-' . $start->format('Ymd') . '-' . $end->format('Ymd') . '.pdf') : ('reporte-ganancias-' . $start->format('Ymd') . '-' . $end->format('Ymd') . '.pdf'); ?> <!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>Reporte de Ganancias | <?= htmlspecialchars($businessName); ?></title> <link rel="icon" href="<?= htmlspecialchars($iconHref); ?>" type="image/png"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="../css/admin_style.css"> <style> body.profit-page { background: linear-gradient(135deg, #f5f9ff 0%, #fff7f3 100%); min-height: 100vh; } .page-heading { display:flex; flex-direction:column; gap:0.85rem; margin-bottom:1.8rem; } .heading-actions { display:flex; gap:0.75rem; flex-wrap:wrap; align-items:center; } .pdf-btn { display: inline-flex; align-items: center; justify-content: center; gap: .55rem; border: 1px solid rgba(148, 163, 184, 0.55); border-radius: 14px; padding: 0.75rem 1.15rem; background: rgba(255, 255, 255, 0.85); color: #0f172a; font-weight: 900; cursor: pointer; text-decoration: none; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.10); backdrop-filter: blur(6px); transition: transform .08s ease, box-shadow .2s ease, filter .2s ease, border-color .2s ease; } .pdf-btn:hover { border-color: rgba(148, 163, 184, 0.75); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); transform: translateY(-1px); } .page-heading h1 { font-size:2.4rem; font-weight:800; color:#111827; display:flex; align-items:center; gap:0.75rem; margin:0; } .page-heading h1 i { color:#0ea5e9; } .page-heading p { margin:0; color:#475569; font-weight:600; } .filters-wrapper { background: rgba(255, 255, 255, 0.86); border-radius: 20px; padding: 1.2rem 1.5rem; box-shadow: 0 18px 42px rgba(17, 24, 39, 0.10); border: 1px solid rgba(226, 232, 240, 0.85); backdrop-filter: blur(10px); margin-bottom: 2rem; display: flex; flex-wrap: wrap; gap: 1.4rem; align-items: flex-start; } .quick-filters { display:flex; flex-wrap:wrap; gap:0.75rem; } .filter-chip { display: inline-flex; align-items: center; gap: 0.55rem; padding: 0.65rem 1.1rem; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.55); font-weight: 800; color: #0f172a; background: rgba(255, 255, 255, 0.85); text-decoration: none; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.10); backdrop-filter: blur(6px); transition: transform .08s ease, box-shadow .2s ease, filter .2s ease, border-color .2s ease; } .filter-chip i { font-size:1rem; } .filter-chip:hover { border-color: rgba(148, 163, 184, 0.75); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); transform: translateY(-1px); } .filter-chip.active { background: linear-gradient(135deg, #0ea5e9, #2563eb); color: #fff; border-color: transparent; box-shadow: 0 16px 32px rgba(37, 99, 235, 0.22); } .custom-range-form { display:flex; flex-wrap:wrap; gap:0.75rem; align-items:flex-end; margin-left:auto; } .custom-range-form label { font-weight:700; color:#1f2937; font-size:0.9rem; display:block; margin-bottom:0.35rem; } .custom-range-form input[type="date"] { border: 1px solid rgba(203, 213, 245, 0.95); border-radius: 12px; padding: 0.65rem 0.9rem; font-size: 0.95rem; min-width: 180px; background: #fff; transition: border-color .2s ease, box-shadow .2s ease; } .custom-range-form input[type="date"]:focus { outline: none; border-color: rgba(14, 165, 233, 0.55); box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.18); } .custom-range-form button { display: inline-flex; align-items: center; justify-content: center; gap: .55rem; border: 1px solid transparent; border-radius: 14px; padding: 0.75rem 1.15rem; background: linear-gradient(135deg, #0ea5e9, #2563eb); color: #fff; font-weight: 900; cursor: pointer; box-shadow: 0 12px 26px rgba(37, 99, 235, 0.22); transition: transform .08s ease, box-shadow .2s ease, filter .2s ease; } .custom-range-form .reset-btn { display: inline-flex; align-items: center; justify-content: center; gap: .55rem; border: 1px solid rgba(148, 163, 184, 0.55); border-radius: 14px; padding: 0.75rem 1.15rem; background: rgba(255, 255, 255, 0.85); color: #0f172a; font-weight: 900; text-decoration: none; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.10); backdrop-filter: blur(6px); transition: transform .08s ease, box-shadow .2s ease, filter .2s ease, border-color .2s ease; } .custom-range-form .reset-btn:hover { border-color: rgba(148, 163, 184, 0.75); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); transform: translateY(-1px); } .custom-range-form button:hover { filter: brightness(1.02); box-shadow: 0 14px 30px rgba(37, 99, 235, 0.26); } .custom-range-form button:active { transform: translateY(1px); } .summary-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1.2rem; margin-bottom:2.2rem; } .summary-card { background: rgba(255, 255, 255, 0.90); border-radius: 20px; padding: 1.3rem 1.4rem; box-shadow: 0 18px 42px rgba(17, 24, 39, 0.10); border: 1px solid rgba(226, 232, 240, 0.85); backdrop-filter: blur(10px); display: flex; flex-direction: column; gap: 0.5rem; position: relative; overflow: hidden; transition: transform .12s ease, box-shadow .2s ease, border-color .2s ease; } .summary-card:hover { transform: translateY(-3px); box-shadow: 0 22px 52px rgba(17, 24, 39, 0.14); border-color: rgba(148, 163, 184, 0.55); } .summary-card.accent { background:linear-gradient(135deg,#0f766e,#14b8a6); color:#fff; } .summary-card h3 { margin:0; font-size:1rem; font-weight:700; color:inherit; opacity:0.85; display:flex; align-items:center; gap:0.45rem; } .summary-value { font-size:2rem; font-weight:800; color:inherit; display:flex; align-items:center; gap:0.45rem; } .summary-meta { font-size:0.9rem; font-weight:600; color:rgba(17,24,39,0.65); } .summary-card.accent .summary-meta { color:rgba(255,255,255,0.8); } .trend-card { background:rgba(255,255,255,0.90); border-radius:22px; padding:1.4rem; box-shadow:0 18px 42px rgba(17,24,39,0.10); border:1px solid rgba(226,232,240,0.85); backdrop-filter: blur(10px); margin-bottom:2.2rem; } .trend-card header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; } .trend-card h2 { margin:0; font-size:1.4rem; font-weight:800; color:#111827; display:flex; align-items:center; gap:0.55rem; } .chart-container { position:relative; width:100%; height:320px; } .channel-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:1.2rem; margin-bottom:2.2rem; } .channel-card { background:rgba(255,255,255,0.90); border-radius:20px; padding:1.3rem 1.4rem; box-shadow:0 18px 42px rgba(17,24,39,0.10); border:1px solid rgba(226,232,240,0.85); backdrop-filter: blur(10px); display:flex; flex-direction:column; gap:0.85rem; } .channel-card header { display:flex; align-items:center; justify-content:space-between; } .channel-card h3 { margin:0; font-size:1.2rem; font-weight:800; color:#111827; display:flex; align-items:center; gap:0.5rem; } .channel-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:0.8rem; } .stat-block { background:rgba(248,250,252,0.85); border-radius:16px; padding:0.85rem 1rem; display:flex; flex-direction:column; gap:0.35rem; border:1px solid rgba(226,232,240,0.95); } .stat-block span { font-size:0.85rem; color:#64748b; font-weight:600; } .stat-block strong { font-size:1.1rem; color:#0f172a; font-weight:800; } .table-section { background:rgba(255,255,255,0.90); border-radius:22px; padding:1.4rem; box-shadow:0 18px 42px rgba(17,24,39,0.10); border:1px solid rgba(226,232,240,0.85); backdrop-filter: blur(10px); margin-bottom:2.4rem; } .table-section h3 { margin:0 0 1rem; font-size:1.35rem; font-weight:800; color:#111827; display:flex; align-items:center; gap:0.55rem; } .table-wrapper { overflow:auto; border-radius:16px; border:1px solid rgba(226,232,240,0.95); background: rgba(255,255,255,0.8); } table { width:100%; border-collapse:collapse; } th, td { padding:0.85rem 1rem; text-align:left; font-size:0.95rem; } th { background:#f8fafc; font-weight:700; color:#0f172a; } tr:nth-child(even) td { background:#fbfcff; } td { font-weight:600; color:#1f2937; } .profit-positive { color:#15803d; } .profit-neutral { color:#6b7280; } .empty-state { text-align:center; padding:1.2rem; font-weight:600; color:#6b7280; } @media (max-width: 1024px) { .channel-grid { grid-template-columns:minmax(0,1fr); } .trend-card { padding:1.1rem; } } @media (max-width: 768px) { .filters-wrapper { flex-direction:column; align-items:stretch; gap:1rem; } .quick-filters { width:100%; overflow-x:auto; padding-bottom:0.2rem; } .quick-filters .filter-chip { flex:0 0 auto; } .custom-range-form { margin-left:0; width:100%; justify-content:space-between; } .custom-range-form > div { flex:1 1 45%; } .custom-range-form button { width:100%; } .custom-range-form .reset-btn { width:100%; } .summary-grid { grid-template-columns:minmax(0,1fr); } .summary-card { padding:1.1rem 1.2rem; } .summary-value { font-size:1.7rem; } .trend-card { margin-bottom:1.8rem; } .chart-container { height:260px; } .channel-grid { gap:1rem; } .channel-card { padding:1.1rem; } .channel-stats { grid-template-columns:minmax(0,1fr); } .custom-range-form { flex-wrap:wrap; row-gap:0.75rem; } .custom-range-form > div { min-width:180px; } } @media (max-width: 560px) { .page-heading h1 { font-size:1.8rem; } .page-heading p { font-size:0.95rem; } .filters-wrapper { padding:1rem; } .quick-filters { gap:0.5rem; } .filter-chip { padding:0.55rem 0.9rem; font-size:0.9rem; } .custom-range-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem; align-content: flex-start; grid-auto-rows: min-content; } .custom-range-form > div { width: 100%; min-width: 0; grid-column: 1 / -1; } .custom-range-form input[type="date"] { width: 100%; min-width: 0; } .custom-range-form button { width: 100%; } .custom-range-form button { grid-column: 1; } .custom-range-form .reset-btn { grid-column: 2; width: 100%; } .trend-card header { flex-direction:column; align-items:flex-start; gap:0.45rem; } .summary-card h3 { font-size:0.95rem; } .summary-value { font-size:1.55rem; } } @media (max-width: 420px) { .custom-range-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } .custom-range-form button, .custom-range-form .reset-btn { padding: 0.7rem 0.8rem; font-size: 0.9rem; } .custom-range-form button { grid-column: 1; } .custom-range-form .reset-btn { grid-column: 2; } .filters-wrapper { gap:0.85rem; } .filter-chip { flex:1 0 auto; text-align:center; } .summary-card { padding:1rem; } .channel-card { padding:1rem; } .channel-card h3 { font-size:1.05rem; } .table-section { padding:1.1rem; } } .profit-pdf-root { position: fixed; left: -10000px; top: 0; width: 794px; background: #ffffff; color: #0f172a; z-index: -1; } .profit-pdf { padding: 36px 40px; font-family: Arial, Helvetica, sans-serif; } .profit-pdf .pdf-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid rgba(226, 232, 240, 0.95); margin-bottom: 18px; } .profit-pdf .pdf-brand { display: flex; align-items: center; gap: 14px; min-width: 0; } .profit-pdf .pdf-logo { width: 58px; height: 58px; border-radius: 16px; object-fit: contain; background: rgba(248, 250, 252, 0.9); border: 1px solid rgba(226, 232, 240, 0.95); padding: 6px; } .profit-pdf .pdf-brand-name { font-weight: 900; font-size: 18px; margin: 0; line-height: 1.1; } .profit-pdf .pdf-brand-meta { margin: 6px 0 0; color: rgba(15, 23, 42, 0.68); font-weight: 700; font-size: 12.5px; } .profit-pdf .pdf-title { text-align: right; } .profit-pdf .pdf-title h2 { margin: 0; font-size: 18px; font-weight: 900; } .profit-pdf .pdf-title p { margin: 6px 0 0; color: rgba(15, 23, 42, 0.68); font-weight: 700; font-size: 12.5px; } .profit-pdf .pdf-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 18px 0; } .profit-pdf .pdf-kpi { border-radius: 16px; border: 1px solid rgba(226, 232, 240, 0.95); background: rgba(248, 250, 252, 0.9); padding: 12px 12px; } .profit-pdf .pdf-kpi span { display: block; font-size: 11.5px; font-weight: 800; color: rgba(15, 23, 42, 0.62); letter-spacing: .2px; } .profit-pdf .pdf-kpi strong { display: block; font-size: 16px; font-weight: 900; margin-top: 6px; } .profit-pdf .pdf-section { margin-top: 18px; } .profit-pdf .pdf-section h3 { margin: 0 0 10px; font-size: 14px; font-weight: 900; color: #0f172a; } .profit-pdf .pdf-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .profit-pdf .pdf-card { border-radius: 16px; border: 1px solid rgba(226, 232, 240, 0.95); background: #ffffff; padding: 12px 12px; } .profit-pdf .pdf-card .row { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px; font-weight: 800; padding: 6px 0; border-bottom: 1px dashed rgba(226, 232, 240, 0.95); } .profit-pdf .pdf-card .row:last-child { border-bottom: none; } .profit-pdf .pdf-table { width: 100%; border-collapse: collapse; border: 1px solid rgba(226, 232, 240, 0.95); border-radius: 0; overflow: visible; page-break-inside: auto; } .profit-pdf .pdf-table thead { display: table-header-group; } .profit-pdf .pdf-table tfoot { display: table-footer-group; } .profit-pdf .pdf-table tr { page-break-inside: avoid; break-inside: avoid; } .profit-pdf .pdf-section.pdf-keep { page-break-inside: avoid; break-inside: avoid-page; } .profit-pdf .pdf-table th, .profit-pdf .pdf-table td { padding: 10px 10px; font-size: 12px; text-align: left; } .profit-pdf .pdf-table th.num, .profit-pdf .pdf-table td.num { text-align: right; } .profit-pdf .pdf-table th { background: rgba(248, 250, 252, 0.95); font-weight: 900; color: #0f172a; } .profit-pdf .pdf-table tr:nth-child(even) td { background: rgba(251, 252, 255, 0.9); } .profit-pdf .pdf-foot { margin-top: 18px; padding-top: 12px; border-top: 1px solid rgba(226, 232, 240, 0.95); color: rgba(15, 23, 42, 0.62); font-size: 11.5px; font-weight: 700; display: flex; justify-content: space-between; gap: 12px; } .profit-pdf .pdf-chart { width: 100%; border-radius: 16px; border: 1px solid rgba(226, 232, 240, 0.95); background: rgba(248, 250, 252, 0.9); padding: 10px; } .profit-pdf .pdf-chart img { width: 100%; height: auto; display: block; } </style> </head> <body class="profit-page"> <?php include '../components/admin_header.php'; ?> <section class="page-heading"> <h1><i class="fas fa-sack-dollar"></i>Reporte de ganancias</h1> <p><?= htmlspecialchars($rangeLabel, ENT_QUOTES, 'UTF-8'); ?> · <?= htmlspecialchars($rangeDescription, ENT_QUOTES, 'UTF-8'); ?></p> <div class="heading-actions"> <button type="button" class="pdf-btn" id="downloadProfitPdf"><i class="fas fa-file-pdf"></i> Descargar PDF</button> </div> </section> <section class="filters-wrapper"> <div class="quick-filters"> <?php foreach($quickRanges as $key => $data): ?> <a href="?range=<?= $key; ?>" class="filter-chip <?= $range === $key ? 'active' : ''; ?>"> <i class="fas <?= $data['icon']; ?>"></i><?= htmlspecialchars($data['label'], ENT_QUOTES, 'UTF-8'); ?> </a> <?php endforeach; ?> </div> <form method="get" class="custom-range-form"> <input type="hidden" name="range" value="custom"> <div> <label for="startDate">Desde</label> <input id="startDate" type="date" name="start_date" value="<?= htmlspecialchars($formStartValue, ENT_QUOTES, 'UTF-8'); ?>"> </div> <div> <label for="endDate">Hasta</label> <input id="endDate" type="date" name="end_date" value="<?= htmlspecialchars($formEndValue, ENT_QUOTES, 'UTF-8'); ?>"> </div> <button type="submit"><i class="fas fa-filter"></i> Aplicar</button> <a class="reset-btn" href="profit_reports.php"><i class="fas fa-undo"></i> Restablecer</a> </form> </section> <section class="summary-grid"> <article class="summary-card accent"> <h3><i class="fas fa-sack-dollar"></i>Utilidad neta</h3> <div class="summary-value"><?= htmlspecialchars(formatMoney($totalProfit, $conn)); ?></div> <span class="summary-meta">Margen promedio: <?= $totalProfitPercent; ?></span> </article> <article class="summary-card"> <h3><i class="fas fa-cash-register"></i>Ingresos totales</h3> <div class="summary-value"><?= htmlspecialchars(formatMoney($totalRevenue, $conn)); ?></div> <span class="summary-meta">Antes de costos</span> </article> <article class="summary-card"> <h3><i class="fas fa-wallet"></i>Costos asociados</h3> <div class="summary-value"><?= htmlspecialchars(formatMoney($totalCost, $conn)); ?></div> <span class="summary-meta">Costos de preparación/insumos</span> </article> <article class="summary-card"> <h3><i class="fas fa-percent"></i>Margen</h3> <div class="summary-value"><?= $totalProfitPercent; ?></div> <span class="summary-meta">Utilidad sobre ingresos</span> </article> </section> <section class="trend-card"> <header> <h2><i class="fas fa-chart-line"></i>Evolución de utilidades</h2> <span style="color:#64748b;font-weight:600;">Ingresos, costos y utilidad por día</span> </header> <div class="chart-container"> <canvas id="profitTrendChart"></canvas> </div> </section> <section class="channel-grid"> <article class="channel-card"> <header> <h3><i class="fas fa-utensils"></i>Comandas pagadas</h3> </header> <div class="channel-stats"> <div class="stat-block"> <span>Ingresos</span> <strong><?= htmlspecialchars(formatMoney($dineSummary['revenue'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Costos</span> <strong><?= htmlspecialchars(formatMoney($dineSummary['cost'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Utilidad</span> <strong class="<?= ($dineSummary['revenue'] - $dineSummary['cost']) >= 0 ? 'profit-positive' : 'profit-neutral'; ?>"><?= htmlspecialchars(formatMoney($dineSummary['revenue'] - $dineSummary['cost'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Pedidos</span> <strong><?= number_format($dineSummary['orders'], 0, ',', '.'); ?></strong> </div> </div> </article> <article class="channel-card"> <header> <h3><i class="fas fa-motorcycle"></i>Pedidos entregados</h3> </header> <div class="channel-stats"> <div class="stat-block"> <span>Ingresos</span> <strong><?= htmlspecialchars(formatMoney($deliverySummary['revenue'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Costos</span> <strong><?= htmlspecialchars(formatMoney($deliverySummary['cost'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Utilidad</span> <strong class="<?= ($deliverySummary['revenue'] - $deliverySummary['cost']) >= 0 ? 'profit-positive' : 'profit-neutral'; ?>"><?= htmlspecialchars(formatMoney($deliverySummary['revenue'] - $deliverySummary['cost'], $conn)); ?></strong> </div> <div class="stat-block"> <span>Pedidos</span> <strong><?= number_format($deliverySummary['orders'], 0, ',', '.'); ?></strong> </div> </div> </article> </section> <section class="table-section"> <h3><i class="fas fa-fire"></i>Top productos por utilidad - Comandas</h3> <div class="table-wrapper"> <table> <thead> <tr> <th>Producto</th> <th>Cant.</th> <th>Ingresos</th> <th>Utilidad</th> </tr> </thead> <tbody> <?php if(empty($topDineProducts)): ?> <tr><td colspan="4" class="empty-state">Sin datos en el rango seleccionado.</td></tr> <?php else: ?> <?php foreach($topDineProducts as $row): $profit = (float)$row['revenue'] - (float)$row['cost']; ?> <tr> <td><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8'); ?></td> <td><?= number_format((float)$row['qty'], 0, ',', '.'); ?></td> <td><?= htmlspecialchars(formatMoney((float)$row['revenue'], $conn)); ?></td> <td class="<?= $profit >= 0 ? 'profit-positive' : 'profit-neutral'; ?>"><?= htmlspecialchars(formatMoney($profit, $conn)); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </section> <section class="table-section"> <h3><i class="fas fa-box-open"></i>Top productos por utilidad - Domicilios</h3> <div class="table-wrapper"> <table> <thead> <tr> <th>Producto</th> <th>Cant.</th> <th>Ingresos</th> <th>Utilidad</th> </tr> </thead> <tbody> <?php if(empty($topDeliveryProducts)): ?> <tr><td colspan="4" class="empty-state">Sin datos en el rango seleccionado.</td></tr> <?php else: ?> <?php foreach($topDeliveryProducts as $row): $profit = (float)$row['revenue'] - (float)$row['cost']; ?> <tr> <td><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8'); ?></td> <td><?= number_format((float)$row['qty'], 0, ',', '.'); ?></td> <td><?= htmlspecialchars(formatMoney((float)$row['revenue'], $conn)); ?></td> <td class="<?= $profit >= 0 ? 'profit-positive' : 'profit-neutral'; ?>"><?= htmlspecialchars(formatMoney($profit, $conn)); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </section> <div class="profit-pdf-root" aria-hidden="true"> <div class="profit-pdf" id="profitReportPdf"> <div class="pdf-header"> <div class="pdf-brand"> <img class="pdf-logo" src="<?= htmlspecialchars($logoHref, ENT_QUOTES, 'UTF-8'); ?>" alt="<?= htmlspecialchars($businessName, ENT_QUOTES, 'UTF-8'); ?>"> <div> <p class="pdf-brand-name"><?= htmlspecialchars($businessName, ENT_QUOTES, 'UTF-8'); ?></p> <p class="pdf-brand-meta"><?= htmlspecialchars($businessPhone, ENT_QUOTES, 'UTF-8'); ?></p> </div> </div> <div class="pdf-title"> <h2>Reporte de ganancias</h2> <p><?= htmlspecialchars($rangeLabel, ENT_QUOTES, 'UTF-8'); ?> · <?= htmlspecialchars($rangeDescription, ENT_QUOTES, 'UTF-8'); ?><br>Emitido: <?= htmlspecialchars($pdfIssuedAt, ENT_QUOTES, 'UTF-8'); ?></p> </div> </div> <div class="pdf-section pdf-keep"> <h3>Resumen general</h3> <table class="pdf-table"> <thead> <tr> <th>Concepto</th> <th class="num">Valor</th> </tr> </thead> <tbody> <tr> <td>Ingresos totales</td> <td class="num"><?= htmlspecialchars(formatMoney($totalRevenue, $conn)); ?></td> </tr> <tr> <td>Costos totales</td> <td class="num"><?= htmlspecialchars(formatMoney($totalCost, $conn)); ?></td> </tr> <tr> <td>Utilidad neta</td> <td class="num"><?= htmlspecialchars(formatMoney($totalProfit, $conn)); ?></td> </tr> <tr> <td>Margen</td> <td class="num"><?= htmlspecialchars($totalProfitPercent, ENT_QUOTES, 'UTF-8'); ?></td> </tr> <tr> <td>Pedidos</td> <td class="num"><?= number_format($totalOrders, 0, ',', '.'); ?></td> </tr> <tr> <td>Ítems vendidos</td> <td class="num"><?= number_format($totalItems, 0, ',', '.'); ?></td> </tr> <tr> <td>Ticket promedio</td> <td class="num"><?= htmlspecialchars(formatMoney($avgTicket, $conn)); ?></td> </tr> </tbody> </table> </div> <div class="pdf-section pdf-keep"> <h3>Resumen por canal</h3> <table class="pdf-table"> <thead> <tr> <th>Canal</th> <th class="num">Pedidos</th> <th class="num">Ítems</th> <th class="num">Ingresos</th> <th class="num">Costos</th> <th class="num">Utilidad</th> <th class="num">Margen</th> </tr> </thead> <tbody> <tr> <td>Comandas pagadas</td> <td class="num"><?= number_format($dineSummary['orders'], 0, ',', '.'); ?></td> <td class="num"><?= number_format($dineSummary['items'], 0, ',', '.'); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($dineSummary['revenue'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($dineSummary['cost'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($dineProfit, $conn)); ?></td> <td class="num"><?= $dineSummary['revenue'] > 0 ? htmlspecialchars(number_format($dineMargin, 2, ',', '.') . '%', ENT_QUOTES, 'UTF-8') : '0%'; ?></td> </tr> <tr> <td>Pedidos entregados</td> <td class="num"><?= number_format($deliverySummary['orders'], 0, ',', '.'); ?></td> <td class="num"><?= number_format($deliverySummary['items'], 0, ',', '.'); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($deliverySummary['revenue'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($deliverySummary['cost'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney($deliveryProfit, $conn)); ?></td> <td class="num"><?= $deliverySummary['revenue'] > 0 ? htmlspecialchars(number_format($deliveryMargin, 2, ',', '.') . '%', ENT_QUOTES, 'UTF-8') : '0%'; ?></td> </tr> </tbody> </table> </div> <div class="pdf-section"> <h3>Evolución diaria</h3> <table class="pdf-table"> <thead> <tr> <th>Fecha</th> <th class="num">Ingresos</th> <th class="num">Costos</th> <th class="num">Utilidad</th> </tr> </thead> <tbody> <?php if(empty($dailyRows)): ?> <tr><td colspan="4" style="padding:12px;color:#64748b;font-weight:700;">Sin datos en el rango seleccionado.</td></tr> <?php else: ?> <?php foreach($dailyRows as $row): ?> <tr> <td><?= htmlspecialchars((string)$row['date'], ENT_QUOTES, 'UTF-8'); ?></td> <td class="num"><?= htmlspecialchars(formatMoney((float)$row['revenue'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney((float)$row['cost'], $conn)); ?></td> <td class="num"><?= htmlspecialchars(formatMoney((float)$row['profit'], $conn)); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="pdf-section"> <h3>Inventario restante (insumos)</h3> <table class="pdf-table"> <thead> <tr> <th>Insumo</th> <th class="num">Disponible</th> <th>Unidad</th> <th class="num">Mínimo</th> <th>Estado</th> </tr> </thead> <tbody> <?php if(empty($inventoryItems)): ?> <tr><td colspan="5" style="padding:12px;color:#64748b;font-weight:700;">No hay insumos registrados.</td></tr> <?php else: ?> <?php foreach($inventoryItems as $item): $stock = (float)($item['stock'] ?? 0); $min = (float)($item['stock_min'] ?? 0); $stockLabel = rtrim(rtrim(number_format($stock, 3, '.', ''), '0'), '.'); $minLabel = rtrim(rtrim(number_format($min, 3, '.', ''), '0'), '.'); $statusLabel = 'OK'; $statusColor = '#15803d'; if ($stock <= 0) { $statusLabel = 'Sin stock'; $statusColor = '#b91c1c'; } elseif ($min > 0 && $stock <= $min) { $statusLabel = 'Bajo'; $statusColor = '#b45309'; } ?> <tr> <td><?= htmlspecialchars((string)$item['name'], ENT_QUOTES, 'UTF-8'); ?></td> <td class="num"><?= htmlspecialchars($stockLabel, ENT_QUOTES, 'UTF-8'); ?></td> <td><?= htmlspecialchars((string)$item['unit'], ENT_QUOTES, 'UTF-8'); ?></td> <td class="num"><?= htmlspecialchars($minLabel, ENT_QUOTES, 'UTF-8'); ?></td> <td><span style="font-weight:900;color:<?= htmlspecialchars($statusColor, ENT_QUOTES, 'UTF-8'); ?>;"><?= htmlspecialchars($statusLabel, ENT_QUOTES, 'UTF-8'); ?></span></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="pdf-section pdf-keep"> <h3>Top productos por utilidad - Comandas</h3> <table class="pdf-table"> <thead> <tr> <th>Producto</th> <th>Cant.</th> <th>Ingresos</th> <th>Utilidad</th> </tr> </thead> <tbody> <?php if(empty($topDineProducts)): ?> <tr><td colspan="4" style="padding:12px;color:#64748b;font-weight:700;">Sin datos en el rango seleccionado.</td></tr> <?php else: ?> <?php foreach($topDineProducts as $row): $profit = (float)$row['revenue'] - (float)$row['cost']; ?> <tr> <td><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8'); ?></td> <td><?= number_format((float)$row['qty'], 0, ',', '.'); ?></td> <td><?= htmlspecialchars(formatMoney((float)$row['revenue'], $conn)); ?></td> <td><?= htmlspecialchars(formatMoney($profit, $conn)); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="pdf-section pdf-keep"> <h3>Top productos por utilidad - Domicilios</h3> <table class="pdf-table"> <thead> <tr> <th>Producto</th> <th>Cant.</th> <th>Ingresos</th> <th>Utilidad</th> </tr> </thead> <tbody> <?php if(empty($topDeliveryProducts)): ?> <tr><td colspan="4" style="padding:12px;color:#64748b;font-weight:700;">Sin datos en el rango seleccionado.</td></tr> <?php else: ?> <?php foreach($topDeliveryProducts as $row): $profit = (float)$row['revenue'] - (float)$row['cost']; ?> <tr> <td><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8'); ?></td> <td><?= number_format((float)$row['qty'], 0, ',', '.'); ?></td> <td><?= htmlspecialchars(formatMoney((float)$row['revenue'], $conn)); ?></td> <td><?= htmlspecialchars(formatMoney($profit, $conn)); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="pdf-foot"> <div>Documento generado desde el panel administrativo.</div> <div><?= htmlspecialchars($businessName, ENT_QUOTES, 'UTF-8'); ?> · <?= htmlspecialchars($businessPhone, ENT_QUOTES, 'UTF-8'); ?></div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> <script> const chartLabels = <?= $chartLabelsJson; ?>; const chartRevenue = <?= $chartRevenueJson; ?>; const chartCost = <?= $chartCostJson; ?>; const chartProfit = <?= $chartProfitJson; ?>; const ctx = document.getElementById('profitTrendChart'); let profitChartInstance = null; if (ctx && chartLabels.length > 0) { profitChartInstance = new Chart(ctx, { type: 'line', data: { labels: chartLabels, datasets: [ { label: 'Ingresos', data: chartRevenue, borderColor: '#14b8a6', backgroundColor: 'rgba(20,184,166,0.12)', borderWidth: 3, tension: 0.35, fill: true, pointRadius: 0 }, { label: 'Costos', data: chartCost, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.12)', borderWidth: 2, tension: 0.35, fill: true, pointRadius: 0 }, { label: 'Utilidad', data: chartProfit, borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.1)', borderWidth: 2, tension: 0.35, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: true, labels: { font: { family: 'Montserrat', weight: '600' } } }, tooltip: { callbacks: { label: (ctx) => { const value = ctx.parsed.y || 0; return ctx.dataset.label + ': ' + (window.formatMoneyJs ? window.formatMoneyJs(value) : value); } } } }, scales: { x: { ticks: { font: { family: 'Montserrat' }, maxRotation: 45, minRotation: 45 } }, y: { ticks: { font: { family: 'Montserrat' }, callback: (value) => (window.formatMoneyJs ? window.formatMoneyJs(value) : value) } } } } }); } const downloadBtn = document.getElementById('downloadProfitPdf'); downloadBtn?.addEventListener('click', async () => { const pdfRoot = document.getElementById('profitReportPdf'); if (!pdfRoot || typeof window.html2pdf === 'undefined') { return; } const opt = { margin: [10, 10, 12, 10], filename: <?= json_encode($pdfFilename, JSON_UNESCAPED_UNICODE); ?>, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'], avoid: ['tr', '.pdf-keep'] } }; await window.html2pdf().set(opt).from(pdfRoot).save(); }); </script> <script src="../js/admin_script.js"></script> </body> </html>
Coded With 💗 by
0x6ick