388 lines
26 KiB
PHP
388 lines
26 KiB
PHP
|
|
<?php
|
||
|
|
$pageTitle = $title ?? 'Sales Reports';
|
||
|
|
$tab = (string)($tab ?? 'overview');
|
||
|
|
$filters = is_array($filters ?? null) ? $filters : [];
|
||
|
|
$overview = is_array($overview ?? null) ? $overview : [];
|
||
|
|
$artistRows = is_array($artist_rows ?? null) ? $artist_rows : [];
|
||
|
|
$trackRows = is_array($track_rows ?? null) ? $track_rows : [];
|
||
|
|
$artistOptions = is_array($artist_options ?? null) ? $artist_options : [];
|
||
|
|
$tablesReady = (bool)($tables_ready ?? false);
|
||
|
|
$currency = (string)($currency ?? 'GBP');
|
||
|
|
$tabLabels = [
|
||
|
|
'overview' => 'Overview',
|
||
|
|
'artists' => 'Artists',
|
||
|
|
'tracks' => 'Tracks',
|
||
|
|
];
|
||
|
|
$activeTabLabel = $tabLabels[$tab] ?? 'Overview';
|
||
|
|
$overviewGross = (float)($overview['gross_revenue'] ?? 0);
|
||
|
|
$overviewRefunded = (float)($overview['refunded_revenue'] ?? 0);
|
||
|
|
$overviewNet = (float)($overview['net_revenue'] ?? 0);
|
||
|
|
$overviewNetAfterFees = (float)($overview['net_after_fees'] ?? 0);
|
||
|
|
$selectedFrom = (string)($filters['from'] ?? '');
|
||
|
|
$selectedTo = (string)($filters['to'] ?? '');
|
||
|
|
$selectedArtistId = (int)($filters['artist_id'] ?? 0);
|
||
|
|
$selectedQuery = (string)($filters['q'] ?? '');
|
||
|
|
$today = new DateTimeImmutable('today');
|
||
|
|
$monthStart = $today->modify('first day of this month');
|
||
|
|
$monthEnd = $today->modify('last day of this month');
|
||
|
|
$lastMonthStart = $monthStart->modify('-1 month');
|
||
|
|
$lastMonthEnd = $monthStart->modify('-1 day');
|
||
|
|
$quarter = (int)floor((((int)$today->format('n')) - 1) / 3);
|
||
|
|
$quarterStartMonth = ($quarter * 3) + 1;
|
||
|
|
$quarterStart = new DateTimeImmutable($today->format('Y') . '-' . str_pad((string)$quarterStartMonth, 2, '0', STR_PAD_LEFT) . '-01');
|
||
|
|
$lastQuarterEnd = $quarterStart->modify('-1 day');
|
||
|
|
$lastQuarterStartMonth = (int)$lastQuarterEnd->format('n') - (((int)$lastQuarterEnd->format('n') - 1) % 3);
|
||
|
|
$lastQuarterStart = new DateTimeImmutable($lastQuarterEnd->format('Y') . '-' . str_pad((string)$lastQuarterStartMonth, 2, '0', STR_PAD_LEFT) . '-01');
|
||
|
|
$quickRanges = [
|
||
|
|
'7d' => ['label' => 'Last 7 days', 'from' => $today->modify('-6 days')->format('Y-m-d'), 'to' => $today->format('Y-m-d')],
|
||
|
|
'30d' => ['label' => 'Last 30 days', 'from' => $today->modify('-29 days')->format('Y-m-d'), 'to' => $today->format('Y-m-d')],
|
||
|
|
'month' => ['label' => 'This month', 'from' => $monthStart->format('Y-m-d'), 'to' => $monthEnd->format('Y-m-d')],
|
||
|
|
'last-month' => ['label' => 'Last month', 'from' => $lastMonthStart->format('Y-m-d'), 'to' => $lastMonthEnd->format('Y-m-d')],
|
||
|
|
'quarter' => ['label' => 'This quarter', 'from' => $quarterStart->format('Y-m-d'), 'to' => $today->format('Y-m-d')],
|
||
|
|
'last-quarter' => ['label' => 'Last quarter', 'from' => $lastQuarterStart->format('Y-m-d'), 'to' => $lastQuarterEnd->format('Y-m-d')],
|
||
|
|
'ytd' => ['label' => 'Year to date', 'from' => $today->format('Y') . '-01-01', 'to' => $today->format('Y-m-d')],
|
||
|
|
];
|
||
|
|
$activeRangeLabel = 'Custom range';
|
||
|
|
foreach ($quickRanges as $range) {
|
||
|
|
if ($selectedFrom === $range['from'] && $selectedTo === $range['to']) {
|
||
|
|
$activeRangeLabel = $range['label'];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$artistExportUrl = '/admin/store/reports/export?type=artists'
|
||
|
|
. '&from=' . rawurlencode($selectedFrom)
|
||
|
|
. '&to=' . rawurlencode($selectedTo)
|
||
|
|
. '&artist_id=' . $selectedArtistId
|
||
|
|
. '&q=' . rawurlencode($selectedQuery);
|
||
|
|
$trackExportUrl = '/admin/store/reports/export?type=tracks'
|
||
|
|
. '&from=' . rawurlencode($selectedFrom)
|
||
|
|
. '&to=' . rawurlencode($selectedTo)
|
||
|
|
. '&artist_id=' . $selectedArtistId
|
||
|
|
. '&q=' . rawurlencode($selectedQuery);
|
||
|
|
$exportUrl = '/admin/store/reports/export?type=' . ($tab === 'tracks' ? 'tracks' : 'artists')
|
||
|
|
. '&from=' . rawurlencode($selectedFrom)
|
||
|
|
. '&to=' . rawurlencode($selectedTo)
|
||
|
|
. '&artist_id=' . $selectedArtistId
|
||
|
|
. '&q=' . rawurlencode($selectedQuery);
|
||
|
|
ob_start();
|
||
|
|
?>
|
||
|
|
<section class="admin-card reports-page reports-shell">
|
||
|
|
<div class="reports-hero">
|
||
|
|
<div class="reports-hero-copy">
|
||
|
|
<div class="badge">Store Analytics</div>
|
||
|
|
<h1>Sales Reports</h1>
|
||
|
|
<p>Revenue, allocations, and performance reporting across orders, artists, releases, and tracks.</p>
|
||
|
|
</div>
|
||
|
|
<div class="reports-hero-actions">
|
||
|
|
<div class="reports-status-card">
|
||
|
|
<span class="reports-status-label">Current View</span>
|
||
|
|
<strong><?= htmlspecialchars($activeTabLabel, ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<small><?= $tablesReady ? 'Live allocation data ready' : 'Waiting for reporting tables' ?></small>
|
||
|
|
</div>
|
||
|
|
<a href="/admin/store" class="btn outline">Back to Store</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if (!$tablesReady): ?>
|
||
|
|
<div class="reports-empty-state">
|
||
|
|
<strong>Reporting tables are not ready.</strong>
|
||
|
|
<span>Initialize the Store plugin first so allocation and reporting tables exist.</span>
|
||
|
|
</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="reports-toolbar">
|
||
|
|
<div class="reports-tabset">
|
||
|
|
<a href="/admin/store/reports?tab=overview" class="reports-tab <?= $tab === 'overview' ? 'is-active' : '' ?>">Overview</a>
|
||
|
|
<a href="/admin/store/reports?tab=artists" class="reports-tab <?= $tab === 'artists' ? 'is-active' : '' ?>">Artists</a>
|
||
|
|
<a href="/admin/store/reports?tab=tracks" class="reports-tab <?= $tab === 'tracks' ? 'is-active' : '' ?>">Tracks</a>
|
||
|
|
</div>
|
||
|
|
<div class="reports-export-actions">
|
||
|
|
<a href="<?= htmlspecialchars($artistExportUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn outline small">Export Artists</a>
|
||
|
|
<a href="<?= htmlspecialchars($trackExportUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn outline small">Export Tracks</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="reports-range-bar">
|
||
|
|
<div class="reports-range-copy">
|
||
|
|
<span class="reports-range-label">Reporting Period</span>
|
||
|
|
<strong><?= htmlspecialchars($activeRangeLabel, ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<small><?= $selectedFrom !== '' || $selectedTo !== '' ? htmlspecialchars(trim(($selectedFrom !== '' ? $selectedFrom : 'Beginning') . ' → ' . ($selectedTo !== '' ? $selectedTo : 'Today')), ENT_QUOTES, 'UTF-8') : 'No explicit date filter applied' ?></small>
|
||
|
|
</div>
|
||
|
|
<div class="reports-range-chips">
|
||
|
|
<?php foreach ($quickRanges as $key => $range): ?>
|
||
|
|
<?php $rangeUrl = '/admin/store/reports?tab=' . rawurlencode($tab) . '&from=' . rawurlencode($range['from']) . '&to=' . rawurlencode($range['to']) . '&artist_id=' . $selectedArtistId . '&q=' . rawurlencode($selectedQuery); ?>
|
||
|
|
<a href="<?= htmlspecialchars($rangeUrl, ENT_QUOTES, 'UTF-8') ?>" class="reports-chip <?= $activeRangeLabel === $range['label'] ? 'is-active' : '' ?>"><?= htmlspecialchars($range['label'], ENT_QUOTES, 'UTF-8') ?></a>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form method="get" action="/admin/store/reports" class="reports-filter-bar">
|
||
|
|
<input type="hidden" name="tab" value="<?= htmlspecialchars($tab, ENT_QUOTES, 'UTF-8') ?>">
|
||
|
|
<label class="reports-filter-field reports-filter-wide">
|
||
|
|
<span>Search</span>
|
||
|
|
<input class="input" type="text" name="q" value="<?= htmlspecialchars($selectedQuery, ENT_QUOTES, 'UTF-8') ?>" placeholder="Artist, track, release, catalog">
|
||
|
|
</label>
|
||
|
|
<label class="reports-filter-field">
|
||
|
|
<span>From</span>
|
||
|
|
<input class="input" type="date" name="from" value="<?= htmlspecialchars($selectedFrom, ENT_QUOTES, 'UTF-8') ?>">
|
||
|
|
</label>
|
||
|
|
<label class="reports-filter-field">
|
||
|
|
<span>To</span>
|
||
|
|
<input class="input" type="date" name="to" value="<?= htmlspecialchars($selectedTo, ENT_QUOTES, 'UTF-8') ?>">
|
||
|
|
</label>
|
||
|
|
<label class="reports-filter-field">
|
||
|
|
<span>Artist</span>
|
||
|
|
<select class="input" name="artist_id">
|
||
|
|
<option value="0">All artists</option>
|
||
|
|
<?php foreach ($artistOptions as $artist): ?>
|
||
|
|
<option value="<?= (int)($artist['id'] ?? 0) ?>" <?= $selectedArtistId === (int)($artist['id'] ?? 0) ? 'selected' : '' ?>><?= htmlspecialchars((string)($artist['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?></option>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<div class="reports-filter-actions">
|
||
|
|
<button type="submit" class="btn small">Apply</button>
|
||
|
|
<a href="/admin/store/reports?tab=<?= rawurlencode($tab) ?>" class="btn outline small">Reset</a>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<?php if ($tab === 'overview'): ?>
|
||
|
|
<div class="reports-kpi-grid reports-kpi-grid-minimal">
|
||
|
|
<article class="reports-metric reports-metric-primary reports-metric-focus">
|
||
|
|
<span>Before Fees</span>
|
||
|
|
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($overviewNet, 2) ?></strong>
|
||
|
|
<small>Total sales before processor fees.</small>
|
||
|
|
</article>
|
||
|
|
<article class="reports-metric reports-metric-primary reports-metric-focus">
|
||
|
|
<span>After Fees</span>
|
||
|
|
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($overviewNetAfterFees, 2) ?></strong>
|
||
|
|
<small>Total sales after captured PayPal fees.</small>
|
||
|
|
</article>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="reports-showcase-grid">
|
||
|
|
<section class="reports-panel reports-ranking-panel">
|
||
|
|
<div class="reports-panel-head">
|
||
|
|
<div>
|
||
|
|
<h2>Top Artists</h2>
|
||
|
|
<p>Best performing artist allocations for the selected period.</p>
|
||
|
|
</div>
|
||
|
|
<span>Net After Fees</span>
|
||
|
|
</div>
|
||
|
|
<?php if (!$overview['top_artists']): ?>
|
||
|
|
<div class="reports-empty-state compact">
|
||
|
|
<strong>No artist sales in this period.</strong>
|
||
|
|
<span>Try widening the date range or clearing the artist filter.</span>
|
||
|
|
</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="reports-ranking-list">
|
||
|
|
<?php foreach ($overview['top_artists'] as $index => $row): ?>
|
||
|
|
<article class="reports-ranking-row">
|
||
|
|
<div class="reports-rank"><?= $index + 1 ?></div>
|
||
|
|
<div class="reports-ranking-copy">
|
||
|
|
<strong><?= htmlspecialchars((string)($row['artist_name'] ?? 'Unknown Artist'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<span><?= (int)($row['units_sold'] ?? 0) ?> sold / <?= (int)($row['paid_orders'] ?? 0) ?> paid orders / <?= (int)($row['release_count'] ?? 0) ?> releases</span>
|
||
|
|
</div>
|
||
|
|
<div class="reports-ranking-value"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['net_after_fees'] ?? 0), 2) ?></div>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="reports-panel reports-ranking-panel">
|
||
|
|
<div class="reports-panel-head">
|
||
|
|
<div>
|
||
|
|
<h2>Top Tracks</h2>
|
||
|
|
<p>Derived track performance from direct, release, and bundle allocations.</p>
|
||
|
|
</div>
|
||
|
|
<span>Net After Fees</span>
|
||
|
|
</div>
|
||
|
|
<?php if (!$overview['top_tracks']): ?>
|
||
|
|
<div class="reports-empty-state compact">
|
||
|
|
<strong>No track sales in this period.</strong>
|
||
|
|
<span>Track-level allocations appear once paid orders exist in the selected range.</span>
|
||
|
|
</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="reports-ranking-list">
|
||
|
|
<?php foreach ($overview['top_tracks'] as $index => $row): ?>
|
||
|
|
<article class="reports-ranking-row reports-ranking-row-track">
|
||
|
|
<div class="reports-rank"><?= $index + 1 ?></div>
|
||
|
|
<div class="reports-ranking-copy">
|
||
|
|
<strong><?= htmlspecialchars((string)($row['track_display'] ?? 'Track'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<span><?= htmlspecialchars((string)($row['artist_name'] ?? 'Unknown Artist'), ENT_QUOTES, 'UTF-8') ?> / <?= htmlspecialchars((string)($row['catalog_no'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||
|
|
</div>
|
||
|
|
<div class="reports-ranking-value"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['net_after_fees'] ?? 0), 2) ?></div>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
<?php elseif ($tab === 'artists'): ?>
|
||
|
|
<section class="reports-panel reports-ledger-panel">
|
||
|
|
<div class="reports-panel-head reports-panel-head-large">
|
||
|
|
<div>
|
||
|
|
<h2>Artist Ledger</h2>
|
||
|
|
<p>Grouped artist performance with paid orders, units, refunds, captured fees, and net after fees.</p>
|
||
|
|
</div>
|
||
|
|
<span><?= count($artistRows) ?> rows</span>
|
||
|
|
</div>
|
||
|
|
<?php if (!$artistRows): ?>
|
||
|
|
<div class="reports-empty-state compact">
|
||
|
|
<strong>No artist sales found.</strong>
|
||
|
|
<span>Try widening the date range or clearing the artist filter.</span>
|
||
|
|
</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="reports-ledger-list">
|
||
|
|
<?php foreach ($artistRows as $row): ?>
|
||
|
|
<article class="reports-ledger-row">
|
||
|
|
<div class="reports-ledger-main">
|
||
|
|
<strong><?= htmlspecialchars((string)($row['artist_name'] ?? 'Unknown Artist'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<span><?= (int)($row['release_count'] ?? 0) ?> releases / <?= (int)($row['track_count'] ?? 0) ?> direct tracks</span>
|
||
|
|
</div>
|
||
|
|
<div class="reports-ledger-metrics">
|
||
|
|
<div class="reports-stat-chip"><small>Paid Orders</small><strong><?= (int)($row['paid_orders'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-stat-chip"><small>Units</small><strong><?= (int)($row['units_sold'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-stat-chip muted"><small>Refunded Units</small><strong><?= (int)($row['units_refunded'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-money-stack"><small>Gross</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['gross_revenue'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack muted"><small>Refunded</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['refunded_revenue'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack muted"><small>PayPal Fees</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['payment_fees'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack emphasis"><small>Net After Fees</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['net_after_fees'] ?? 0), 2) ?></strong></div>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
<?php else: ?>
|
||
|
|
<section class="reports-panel reports-ledger-panel">
|
||
|
|
<div class="reports-panel-head reports-panel-head-large">
|
||
|
|
<div>
|
||
|
|
<h2>Track Ledger</h2>
|
||
|
|
<p>Track-level breakdown derived from direct sales, release sales, bundle allocations, and captured PayPal fees.</p>
|
||
|
|
</div>
|
||
|
|
<span><?= count($trackRows) ?> rows</span>
|
||
|
|
</div>
|
||
|
|
<?php if (!$trackRows): ?>
|
||
|
|
<div class="reports-empty-state compact">
|
||
|
|
<strong>No track sales found.</strong>
|
||
|
|
<span>Track metrics will populate once paid orders exist in the selected range.</span>
|
||
|
|
</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="reports-ledger-list reports-ledger-list-tracks">
|
||
|
|
<?php foreach ($trackRows as $row): ?>
|
||
|
|
<article class="reports-ledger-row reports-ledger-row-track">
|
||
|
|
<div class="reports-ledger-main">
|
||
|
|
<strong><?= htmlspecialchars((string)($row['track_display'] ?? 'Track'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
|
|
<span><?= htmlspecialchars((string)($row['artist_name'] ?? 'Unknown Artist'), ENT_QUOTES, 'UTF-8') ?> / <?= htmlspecialchars((string)($row['release_title'] ?? ''), ENT_QUOTES, 'UTF-8') ?><?= !empty($row['catalog_no']) ? ' / ' . htmlspecialchars((string)$row['catalog_no'], ENT_QUOTES, 'UTF-8') : '' ?></span>
|
||
|
|
</div>
|
||
|
|
<div class="reports-ledger-metrics">
|
||
|
|
<div class="reports-stat-chip"><small>Sold</small><strong><?= (int)($row['units_sold'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-stat-chip muted"><small>Refunded Units</small><strong><?= (int)($row['units_refunded'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-stat-chip"><small>Downloads</small><strong><?= (int)($row['download_count'] ?? 0) ?></strong></div>
|
||
|
|
<div class="reports-money-stack"><small>Gross</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['gross_revenue'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack muted"><small>Refunded</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['refunded_revenue'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack muted"><small>PayPal Fees</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['payment_fees'] ?? 0), 2) ?></strong></div>
|
||
|
|
<div class="reports-money-stack emphasis"><small>Net After Fees</small><strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['net_after_fees'] ?? 0), 2) ?></strong></div>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
<?php endif; ?>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
<style>
|
||
|
|
.reports-shell { display:grid; gap:18px; }
|
||
|
|
.reports-hero { display:grid; grid-template-columns:minmax(0,1fr) 280px; gap:18px; align-items:start; }
|
||
|
|
.reports-hero-copy { display:grid; gap:10px; }
|
||
|
|
.reports-hero-copy h1 { margin:0; font-size:34px; line-height:1; }
|
||
|
|
.reports-hero-copy p { margin:0; max-width:760px; color:var(--muted); font-size:15px; }
|
||
|
|
.reports-hero-actions { display:grid; gap:12px; justify-items:stretch; }
|
||
|
|
.reports-status-card { padding:16px; border-radius:16px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.015)); box-shadow:inset 0 1px 0 rgba(255,255,255,.04); }
|
||
|
|
.reports-status-card strong { display:block; font-size:20px; margin-top:6px; }
|
||
|
|
.reports-status-card small, .reports-status-label { color:var(--muted); display:block; }
|
||
|
|
.reports-status-label { font-size:11px; text-transform:uppercase; letter-spacing:.16em; }
|
||
|
|
.reports-toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
|
||
|
|
.reports-tabset { display:flex; flex-wrap:wrap; gap:8px; }
|
||
|
|
.reports-export-actions { display:flex; gap:8px; flex-wrap:wrap; }
|
||
|
|
.reports-tab { padding:10px 14px; border-radius:999px; border:1px solid rgba(255,255,255,.08); color:var(--muted); text-decoration:none; font-size:12px; letter-spacing:.16em; text-transform:uppercase; background:rgba(255,255,255,.02); }
|
||
|
|
.reports-tab.is-active { color:#0b1015; background:linear-gradient(135deg, #48d3ff, #31f0a8); border-color:transparent; box-shadow:0 12px 30px rgba(49,240,168,.18); }
|
||
|
|
.reports-range-bar { display:grid; grid-template-columns:minmax(220px,.9fr) minmax(0,2.1fr); gap:16px; align-items:start; padding:16px; border-radius:18px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.012)); }
|
||
|
|
.reports-range-copy { display:grid; gap:4px; }
|
||
|
|
.reports-range-label { color:var(--muted); font-size:11px; letter-spacing:.16em; text-transform:uppercase; }
|
||
|
|
.reports-range-copy strong { font-size:22px; line-height:1.1; }
|
||
|
|
.reports-range-copy small { color:var(--muted); }
|
||
|
|
.reports-range-chips { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-start; align-content:flex-start; }
|
||
|
|
.reports-chip { padding:9px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.08); color:var(--muted); text-decoration:none; font-size:11px; letter-spacing:.12em; text-transform:uppercase; background:rgba(255,255,255,.02); }
|
||
|
|
.reports-chip.is-active { color:#edf6ff; border-color:rgba(72,211,255,.28); background:rgba(72,211,255,.12); }
|
||
|
|
.reports-filter-bar { display:grid; grid-template-columns:minmax(280px,1.6fr) repeat(3, minmax(160px, .8fr)) auto; gap:12px; align-items:end; padding:16px; border-radius:18px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015)); }
|
||
|
|
.reports-filter-field { display:grid; gap:8px; }
|
||
|
|
.reports-filter-field span { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.14em; }
|
||
|
|
.reports-filter-actions { display:flex; gap:8px; align-items:center; justify-content:flex-end; flex-wrap:wrap; }
|
||
|
|
.reports-kpi-grid { display:grid; grid-template-columns:repeat(7, minmax(0,1fr)); gap:12px; }
|
||
|
|
.reports-kpi-grid-minimal { grid-template-columns:repeat(2, minmax(0,1fr)); }
|
||
|
|
.reports-metric { padding:18px; border-radius:18px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.015)); display:grid; gap:10px; }
|
||
|
|
.reports-metric span { color:var(--muted); font-size:11px; letter-spacing:.16em; text-transform:uppercase; }
|
||
|
|
.reports-metric strong { font-size:28px; line-height:1; }
|
||
|
|
.reports-metric small { color:var(--muted); font-size:12px; }
|
||
|
|
.reports-metric-primary strong { font-size:32px; }
|
||
|
|
.reports-metric-focus { min-height:150px; }
|
||
|
|
.reports-showcase-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
|
||
|
|
.reports-panel { padding:18px; border-radius:20px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015)); }
|
||
|
|
.reports-panel-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:14px; }
|
||
|
|
.reports-panel-head h2 { margin:0; font-size:22px; }
|
||
|
|
.reports-panel-head p { margin:6px 0 0; color:var(--muted); max-width:560px; }
|
||
|
|
.reports-panel-head span { color:var(--muted); font-size:11px; letter-spacing:.16em; text-transform:uppercase; white-space:nowrap; }
|
||
|
|
.reports-panel-head-large { margin-bottom:18px; }
|
||
|
|
.reports-ranking-list, .reports-ledger-list { display:grid; gap:10px; }
|
||
|
|
.reports-ranking-row { display:grid; grid-template-columns:46px minmax(0,1fr) auto; gap:14px; align-items:center; padding:14px 16px; border-radius:16px; border:1px solid rgba(255,255,255,.07); background:rgba(255,255,255,.025); }
|
||
|
|
.reports-rank { width:46px; height:46px; border-radius:14px; display:grid; place-items:center; font-size:20px; font-weight:800; color:#d8eef9; background:linear-gradient(180deg, rgba(72,211,255,.18), rgba(49,240,168,.08)); border:1px solid rgba(72,211,255,.18); }
|
||
|
|
.reports-ranking-copy { display:grid; gap:4px; min-width:0; }
|
||
|
|
.reports-ranking-copy strong { font-size:16px; }
|
||
|
|
.reports-ranking-copy span { color:var(--muted); font-size:13px; }
|
||
|
|
.reports-ranking-value { font-size:20px; font-weight:800; white-space:nowrap; }
|
||
|
|
.reports-ledger-panel { display:grid; gap:12px; }
|
||
|
|
.reports-ledger-row { display:grid; grid-template-columns:minmax(220px,1.15fr) minmax(0,2.2fr); gap:14px; align-items:start; padding:16px; border-radius:18px; border:1px solid rgba(255,255,255,.07); background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.012)); }
|
||
|
|
.reports-ledger-row-track { grid-template-columns:minmax(280px,1.25fr) minmax(0,2.3fr); }
|
||
|
|
.reports-ledger-main { display:grid; gap:5px; align-content:center; min-width:0; }
|
||
|
|
.reports-ledger-main strong { font-size:18px; }
|
||
|
|
.reports-ledger-main span { color:var(--muted); font-size:13px; }
|
||
|
|
.reports-ledger-metrics { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:10px; }
|
||
|
|
.reports-stat-chip, .reports-money-stack { padding:12px 14px; border-radius:14px; background:rgba(255,255,255,.03); border:1px solid rgba(255,255,255,.05); display:grid; gap:6px; align-content:center; }
|
||
|
|
.reports-stat-chip small, .reports-money-stack small { color:var(--muted); font-size:10px; letter-spacing:.14em; text-transform:uppercase; }
|
||
|
|
.reports-stat-chip strong, .reports-money-stack strong { font-size:20px; line-height:1; }
|
||
|
|
.reports-money-stack.emphasis { background:linear-gradient(180deg, rgba(72,211,255,.12), rgba(49,240,168,.08)); border-color:rgba(72,211,255,.18); }
|
||
|
|
.reports-stat-chip.muted, .reports-money-stack.muted { opacity:.78; }
|
||
|
|
.reports-empty-state { padding:16px 18px; border-radius:16px; border:1px dashed rgba(255,255,255,.12); background:rgba(255,255,255,.015); display:grid; gap:6px; color:var(--muted); }
|
||
|
|
.reports-empty-state strong { color:#edf6ff; font-size:15px; }
|
||
|
|
.reports-empty-state.compact { min-height:132px; align-content:center; }
|
||
|
|
@media (max-width: 1380px) {
|
||
|
|
.reports-hero { grid-template-columns:1fr; }
|
||
|
|
.reports-kpi-grid { grid-template-columns:repeat(3, minmax(0,1fr)); }
|
||
|
|
.reports-range-bar { grid-template-columns:1fr; }
|
||
|
|
.reports-filter-bar { grid-template-columns:repeat(2, minmax(0,1fr)); }
|
||
|
|
.reports-filter-wide, .reports-filter-actions { grid-column:1 / -1; }
|
||
|
|
.reports-filter-actions { justify-content:flex-start; }
|
||
|
|
.reports-showcase-grid { grid-template-columns:1fr; }
|
||
|
|
.reports-ledger-row, .reports-ledger-row-track { grid-template-columns:1fr; }
|
||
|
|
}
|
||
|
|
@media (max-width: 920px) {
|
||
|
|
.reports-kpi-grid { grid-template-columns:repeat(2, minmax(0,1fr)); }
|
||
|
|
.reports-ranking-row { grid-template-columns:40px minmax(0,1fr); }
|
||
|
|
.reports-ranking-value { grid-column:2; }
|
||
|
|
.reports-ledger-metrics { grid-template-columns:repeat(2, minmax(0,1fr)); }
|
||
|
|
}
|
||
|
|
@media (max-width: 640px) {
|
||
|
|
.reports-kpi-grid, .reports-filter-bar, .reports-ledger-row, .reports-ledger-row-track, .reports-ledger-metrics { grid-template-columns:1fr; }
|
||
|
|
.reports-toolbar { align-items:flex-start; }
|
||
|
|
.reports-range-chips, .reports-export-actions { width:100%; }
|
||
|
|
.reports-ranking-row { grid-template-columns:1fr; }
|
||
|
|
.reports-rank { width:38px; height:38px; }
|
||
|
|
.reports-ranking-value { grid-column:auto; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
<?php
|
||
|
|
$content = ob_get_clean();
|
||
|
|
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|