diff --git a/src/Infrastructure/ResellerTableRenderer.php b/src/Infrastructure/ResellerTableRenderer.php index aa79e327..9a9f124a 100644 --- a/src/Infrastructure/ResellerTableRenderer.php +++ b/src/Infrastructure/ResellerTableRenderer.php @@ -625,7 +625,7 @@ class ResellerTableRenderer { } $rReturn['recordsFiltered'] = $rReturn['recordsTotal']; if (0 < $rReturn['recordsTotal']) { - $rQuery = 'SELECT `id`, `stream_icon`, `stream_display_name`, `tv_archive_duration`, `tv_archive_server_id`, `category_id`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; + $rQuery = 'SELECT `id`, `stream_icon`, `stream_display_name`, `added`, `tv_archive_duration`, `tv_archive_server_id`, `category_id`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; $db->query($rQuery, ...$rWhereV); if ($db->num_rows() > 0) { $rRows = $db->get_rows(); @@ -664,6 +664,7 @@ class ResellerTableRenderer { 'id' => (int) $rRow['id'], 'icon' => (string) $rRow['stream_icon'], 'title' => (string) $rRow['stream_display_name'], + 'added' => (int) $rRow['added'], 'archive' => 0 < $rRow['tv_archive_duration'] && 0 < $rRow['tv_archive_server_id'], 'category' => $rCategory, 'clients' => (int) $rRow['clients'], @@ -854,7 +855,7 @@ class ResellerTableRenderer { } $rReturn['recordsFiltered'] = $rReturn['recordsTotal']; if (0 < $rReturn['recordsTotal']) { - $rQuery = 'SELECT `id`, `stream_icon`, `stream_display_name`, `movie_properties`, `category_id`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; + $rQuery = 'SELECT `id`, `stream_icon`, `stream_display_name`, `added`, `movie_properties`, `category_id`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; $db->query($rQuery, ...$rWhereV); if (0 < $db->num_rows()) { $rRows = $db->get_rows(); @@ -894,6 +895,7 @@ class ResellerTableRenderer { 'id' => (int) $rRow['id'], 'image' => (string) ($rProperties['movie_image'] ?? ''), 'title' => (string) $rRow['stream_display_name'], + 'added' => (int) $rRow['added'], 'category' => $rCategory, 'clients' => (int) $rRow['clients'], ]; @@ -977,7 +979,7 @@ class ResellerTableRenderer { } $rReturn['recordsFiltered'] = $rReturn['recordsTotal']; if (0 < $rReturn['recordsTotal']) { - $rQuery = 'SELECT `streams`.`id`, `stream_icon`, `stream_display_name`, `movie_properties`, `streams_series`.`category_id`, `streams_series`.`title`, `streams_episodes`.`season_num`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` LEFT JOIN `streams_episodes` ON `streams_episodes`.`stream_id` = `streams`.`id` LEFT JOIN `streams_series` ON `streams_series`.`id` = `streams_episodes`.`series_id` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; + $rQuery = 'SELECT `streams`.`id`, `stream_icon`, `stream_display_name`, `streams`.`added`, `movie_properties`, `streams_series`.`category_id`, `streams_series`.`title`, `streams_episodes`.`season_num`, (SELECT COUNT(*) FROM `lines_live` LEFT JOIN `lines` ON `lines`.`id` = `lines_live`.`user_id` WHERE `lines_live`.`stream_id` = `streams`.`id` AND `hls_end` = 0 AND `lines`.`member_id` IN (' . implode(',', $rUserInfo['reports']) . ')) AS `clients` FROM `streams` LEFT JOIN `streams_episodes` ON `streams_episodes`.`stream_id` = `streams`.`id` LEFT JOIN `streams_series` ON `streams_series`.`id` = `streams_episodes`.`series_id` ' . $rWhereString . ' ' . $rOrderBy . ' LIMIT ' . $rStart . ', ' . $rLimit . ';'; $db->query($rQuery, ...$rWhereV); if (0 < $db->num_rows()) { $rRows = $db->get_rows(); @@ -1018,6 +1020,7 @@ class ResellerTableRenderer { 'id' => (int) $rRow['id'], 'image' => (string) ($rProperties['movie_image'] ?? ''), 'title' => (string) $rRow['stream_display_name'], + 'added' => (int) $rRow['added'], 'series' => (string) $rRow['title'], 'season' => ($rRow['season_num'] !== null ? (int) $rRow['season_num'] : null), 'category' => $rCategory, diff --git a/src/Public/Views/reseller/dashboard.php b/src/Public/Views/reseller/dashboard.php index cd091aea..3f20af40 100644 --- a/src/Public/Views/reseller/dashboard.php +++ b/src/Public/Views/reseller/dashboard.php @@ -79,7 +79,10 @@ foreach ($rConnectionMap as $rCountry) {
-
+
+ + +
@@ -115,7 +118,10 @@ foreach ($rConnectionMap as $rCountry) {
-
+
+ + +
@@ -147,7 +153,10 @@ foreach ($rConnectionMap as $rCountry) {
-
+
+ + +
@@ -179,7 +188,10 @@ foreach ($rConnectionMap as $rCountry) {
-
+
+ + +
@@ -217,34 +229,42 @@ foreach ($rConnectionMap as $rCountry) {
+
-
+
+ + +
- [$rPaneId, $rTableId, $rListUrl, $rImageLabel]): - ?> + [$rAction, $rPaneId, $rTableId, $rTabIcon, $rRowIcon, $rAccent, $rImageLabel, $rImageWidth]): ?>
@@ -255,14 +275,21 @@ foreach ($rConnectionMap as $rCountry) { + + + +
@@ -326,6 +353,8 @@ renderUnifiedLayoutFooter('reseller'); kill: , error: }; + var langSeason = ; + var langConnections = ; // Same colour-coded live duration the Live Connections page renders. function fmtDuration(startTs, isRestreamer) { @@ -380,11 +409,102 @@ renderUnifiedLayoutFooter('reseller'); }); } + var canSeeConnections = ; + + function fmtDate(ts) { + return ts ? new Date(ts * 1000).toLocaleString() : '—'; + } + // Media panels: the Streams / Movies / Episodes endpoints share one - // column contract (id, image, title, category, clients), so one factory - // covers all three. Read-only here — killing lives on the full pages. - function mediaTable(selector, action, imageWidth) { - return jQuery(selector).DataTable({ + // column contract (id, image, title, added, category, clients), so one + // factory covers all three. The only per-stream action a reseller has is + // its live connections — there is no reseller stream_view route. + function mediaTable(tab) { + var columns = [{ + data: null, + defaultContent: '', + orderable: false, + searchable: false, + className: 'control', + responsivePriority: 2 + }, + { + data: 'id', + className: 'text-center', + render: function(d) { + return '' + esc(d) + ''; + } + }, + { + data: tab.action === 'streams' ? 'icon' : 'image', + orderable: false, + searchable: false, + className: 'text-center', + render: function(d) { + if (d) { + return ''; + } + // Artwork is optional; a tinted type badge keeps the column + // from collapsing into an empty cell. + return ''; + } + }, + { + data: 'title', + responsivePriority: 1, + render: function(d, t, row) { + var sub = ''; + if (row.series) { + sub = esc(row.series) + (row.season != null && row.season !== '' ? ' — ' + esc(langSeason) + ' ' + esc(row.season) : ''); + sub = '
' + sub + ''; + } + return '' + esc(d) + '' + sub; + } + }, + { + data: 'category', + render: function(d) { + if (!d) { + return '—'; + } + return '' + esc(d) + ''; + } + }, + { + data: 'added', + orderable: false, + searchable: false, + className: 'text-center text-nowrap', + render: function(d) { + return '' + esc(fmtDate(d)) + ''; + } + }, + { + data: 'clients', + orderable: false, + searchable: false, + className: 'text-center', + render: function(d) { + return '' + (d || 0) + ''; + } + } + ]; + + if (canSeeConnections) { + columns.push({ + data: null, + orderable: false, + searchable: false, + className: 'text-center', + render: function(d, t, row) { + return '' + + ''; + } + }); + } + + var table = jQuery('#' + tab.tableId).DataTable({ serverSide: true, responsive: { details: { @@ -392,6 +512,8 @@ renderUnifiedLayoutFooter('reseller'); target: 0 } }, + // Newest first. `added` is not orderable — the endpoint maps the + // sort column by index and only knows id, title and category. order: [ [1, 'desc'] ], @@ -401,63 +523,25 @@ renderUnifiedLayoutFooter('reseller'); ajax: { url: './table', data: function(d) { - d.id = action; + d.id = tab.action; } }, - columnDefs: [{ - orderable: false, - targets: [0, 2] - }], - columns: [{ - data: null, - defaultContent: '', - orderable: false, - searchable: false, - className: 'control', - responsivePriority: 2 - }, - { - data: 'id', - className: 'text-center', - render: function(d) { - return '' + esc(d) + ''; - } - }, - { - data: action === 'streams' ? 'icon' : 'image', - orderable: false, - searchable: false, - render: function(d) { - return d ? '' : ''; - } - }, - { - data: 'title', - responsivePriority: 1, - render: function(d, t, row) { - var sub = row.series ? '
' + esc(row.series) + '' : ''; - return '' + esc(d) + '' + sub; - } - }, - { - data: 'category', - render: function(d) { - return '' + esc(d || '') + ''; - } - }, - { - data: 'clients', - className: 'text-center', - render: function(d) { - return '' + (d || 0) + ''; - } - } - ], + columns: columns, layout: { topStart: 'pageLength', topEnd: 'search' } }); + + // The tab badge shows the real catalogue size, not the page length. + table.on('xhr', function(e, settings, json) { + var badge = document.getElementById('count-' + tab.tableId); + if (badge && json) { + badge.textContent = nf.format(json.recordsTotal || 0); + } + }); + + return table; } @@ -606,9 +690,15 @@ renderUnifiedLayoutFooter('reseller'); - mediaTable('#dash-streams-table', 'streams', 96); - mediaTable('#dash-movies-table', 'movies', 32); - mediaTable('#dash-episodes-table', 'episodes', 32); + + mediaTable( $rAction, + 'tableId' => $rTableId, + 'rowIcon' => $rRowIcon, + 'accent' => $rAccent, + 'imageWidth' => $rImageWidth, + ], JSON_UNESCAPED_SLASHES); ?>); + // Server-rendered panels: client-side DataTables for sort, search and paging.