Move requests report into Vue component.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-09-06 04:06:31 -05:00
parent 8d3cab6e76
commit c94e2edf19
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
30 changed files with 375 additions and 217 deletions

View File

@ -597,6 +597,12 @@ return [
// Auto-managed by Assets
],
'Vue_StationsReportsRequests' => [
'order' => 10,
'require' => ['vue-component-common', 'uses-api', 'bootstrap-vue', 'moment'],
// Auto-managed by Assets
],
'Vue_StationsReportsOverview' => [
'order' => 10,
'require' => ['vue-component-common', 'uses-api', 'bootstrap-vue', 'chartjs'],

View File

@ -243,10 +243,8 @@ return static function (RouteCollectorProxy $app) {
$group->post('/clear', Controller\Api\Stations\QueueController::class . ':clearAction')
->setName('api:stations:queue:clear');
$group->get('/{id}', Controller\Api\Stations\QueueController::class . ':getAction')
$group->delete('/{id}', Controller\Api\Stations\QueueController::class . ':deleteAction')
->setName('api:stations:queue:record');
$group->delete('/{id}', Controller\Api\Stations\QueueController::class . ':deleteAction');
}
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
@ -572,6 +570,26 @@ return static function (RouteCollectorProxy $app) {
$group->group(
'/reports',
function (RouteCollectorProxy $group) {
$group->group(
'/requests',
function (RouteCollectorProxy $group) {
$group->get(
'',
Controller\Api\Stations\Reports\RequestsController::class . ':listAction'
)->setName('api:stations:reports:requests');
$group->post(
'/clear',
Controller\Api\Stations\Reports\RequestsController::class . ':clearAction'
)->setName('api:stations:reports:requests:clear');
$group->delete(
'/{request_id}',
Controller\Api\Stations\Reports\RequestsController::class . ':deleteAction'
)->setName('api:stations:reports:requests:delete');
}
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
$group->get(
'/overview/charts',
Controller\Api\Stations\Reports\Overview\ChartsAction::class

View File

@ -109,20 +109,8 @@ return static function (RouteCollectorProxy $app) {
)
->setName('stations:reports:soundexchange');
$group->get('/requests', Controller\Stations\Reports\RequestsController::class)
$group->get('/requests', Controller\Stations\Reports\RequestsAction::class)
->setName('stations:reports:requests');
$group->get(
'/requests/delete/{request_id}/{csrf}',
Controller\Stations\Reports\RequestsController::class . ':deleteAction'
)
->setName('stations:reports:requests:delete');
$group->get(
'/requests/clear/{csrf}',
Controller\Stations\Reports\RequestsController::class . ':clearAction'
)
->setName('stations:reports:requests:clear');
}
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));

View File

@ -10,7 +10,7 @@
<translate key="lang_btn_clear_requests">Clear Upcoming Song Queue</translate>
</b-button>
</div>
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl" handle-client-side>
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl">
<template #cell(actions)="row">
<b-button-group>
<b-button v-if="row.item.log" size="sm" variant="primary"
@ -60,12 +60,11 @@ import handleAxiosError from '../Function/handleAxiosError';
import Icon from "../Common/Icon";
export default {
name: 'StationPlaylists',
name: 'StationQueue',
components: {QueueLogsModal, DataTable, Icon},
props: {
listUrl: String,
clearUrl: String,
locale: String,
stationTimeZone: String
},
data() {

View File

@ -0,0 +1,130 @@
<template>
<div>
<b-card no-body>
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title" key="lang_queue" v-translate>Song Requests</h2>
</b-card-header>
<div class="card-actions">
<b-button variant="outline-danger" @click="doClear()">
<icon icon="remove"></icon>
<translate key="lang_btn_clear_requests">Clear Pending Requests</translate>
</b-button>
</div>
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl">
<template #cell(timestamp)="row">
{{ formatTime(row.item.timestamp) }}
</template>
<template #cell(played_at)="row">
<span v-if="row.item.played_at === 0">
<translate key="lang_item_not_played">Not Played</translate>
</span>
<span v-else>
{{ formatTime(row.item.played_at) }}
</span>
</template>
<template #cell(song_title)="row">
<div v-if="row.item.track.title">
<b>{{ row.item.track.title }}</b><br>
{{ row.item.track.artist }}
</div>
<div v-else>
{{ row.item.track.text }}
</div>
</template>
<template #cell(ip)="row">
{{ row.item.ip }}
</template>
<template #cell(actions)="row">
<b-button-group>
<b-button v-if="row.item.played_at === 0" size="sm" variant="danger"
@click.prevent="doDelete(row.item.links.delete)">
<translate key="lang_btn_delete">Delete</translate>
</b-button>
</b-button-group>
</template>
</data-table>
</b-card>
</div>
</template>
<script>
import DataTable from '../../Common/DataTable';
import handleAxiosError from '../../Function/handleAxiosError';
import Icon from "../../Common/Icon";
export default {
name: 'StationRequests',
components: {DataTable, Icon},
props: {
listUrl: String,
clearUrl: String,
stationTimeZone: String
},
data() {
return {
fields: [
{key: 'timestamp', label: this.$gettext('Date Requested'), sortable: false},
{key: 'played_at', label: this.$gettext('Date Played'), sortable: false},
{key: 'song_title', isRowHeader: true, label: this.$gettext('Song Title'), sortable: false},
{key: 'ip', label: this.$gettext('Requester IP'), sortable: false},
{key: 'actions', label: this.$gettext('Actions'), sortable: false}
]
}
},
mounted() {
moment.relativeTimeThreshold('ss', 1);
moment.relativeTimeRounding(function (value) {
return Math.round(value * 10) / 10;
});
},
methods: {
formatTime(time) {
return moment.unix(time).tz(this.stationTimeZone).format('lll');
},
doDelete(url) {
let buttonText = this.$gettext('Delete');
let buttonConfirmText = this.$gettext('Delete request?');
Swal.fire({
title: buttonConfirmText,
confirmButtonText: buttonText,
confirmButtonColor: '#e64942',
showCancelButton: true,
focusCancel: true
}).then((result) => {
if (result.value) {
this.axios.delete(url).then((resp) => {
notify('<b>' + resp.data.message + '</b>', 'success');
this.$refs.datatable.refresh();
}).catch((err) => {
handleAxiosError(err);
});
}
});
},
doClear() {
let buttonText = this.$gettext('Clear');
let buttonConfirmText = this.$gettext('Clear all pending requests?');
Swal.fire({
title: buttonConfirmText,
confirmButtonText: buttonText,
confirmButtonColor: '#e64942',
showCancelButton: true,
focusCancel: true
}).then((result) => {
if (result.value) {
this.axios.post(this.clearUrl).then((resp) => {
notify('<b>' + resp.data.message + '</b>', 'success');
this.$refs.datatable.refresh();
}).catch((err) => {
handleAxiosError(err);
});
}
});
}
}
};
</script>

View File

@ -24,6 +24,7 @@ module.exports = {
StationsQueue: './vue/Stations/Queue.vue',
StationsRemotes: './vue/Stations/Remotes.vue',
StationsStreamers: './vue/Stations/Streamers.vue',
StationsReportsRequests: './vue/Stations/Reports/Requests.vue',
StationsReportsOverview: './vue/Stations/Reports/Overview.vue'
},
resolve: {

View File

@ -44,14 +44,14 @@ abstract class AbstractApiCrudController
): ResponseInterface {
$paginator = Paginator::fromQuery($query, $request);
$is_bootgrid = $paginator->isFromBootgrid();
$is_internal = ('true' === $request->getParam('internal', 'false'));
$isBootgrid = $paginator->isFromBootgrid();
$isInternal = ('true' === $request->getParam('internal', 'false'));
$postProcessor ??= function ($row) use ($is_bootgrid, $is_internal, $request) {
$postProcessor ??= function ($row) use ($isBootgrid, $isInternal, $request) {
$return = $this->viewRecord($row, $request);
// Older jQuery Bootgrid requests should be "flattened".
if ($is_bootgrid && !$is_internal) {
if ($isBootgrid && !$isInternal) {
return Utilities\Arrays::flattenArray($return, '_');
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Reports;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
class RequestsController
{
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\StationRequestRepository $requestRepo
) {
}
public function listAction(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
$requests = $this->em->createQuery(
<<<'DQL'
SELECT sr, sm
FROM App\Entity\StationRequest sr
JOIN sr.track sm
WHERE sr.station = :station
ORDER BY sr.timestamp DESC
DQL
)->setParameter('station', $station)
->getArrayResult();
$paginator = Paginator::fromArray($requests, $request);
$router = $request->getRouter();
$postProcessor = function ($row) use ($router) {
$row['links'] = [
'delete' => (string)$router->fromHere(
'api:stations:reports:requests:delete',
['request_id' => $row['id']]
),
];
return $row;
};
$paginator->setPostprocessor($postProcessor);
return $paginator->write($response);
}
public function deleteAction(
ServerRequest $request,
Response $response,
int $request_id
): ResponseInterface {
$station = $request->getStation();
$media = $this->requestRepo->getPendingRequest($request_id, $station);
if ($media instanceof Entity\StationRequest) {
$this->em->remove($media);
$this->em->flush();
}
return $response->withJson(Entity\Api\Status::deleted());
}
public function clearAction(
ServerRequest $request,
Response $response,
): ResponseInterface {
$station = $request->getStation();
$this->requestRepo->clearPendingRequests($station);
return $response->withJson(Entity\Api\Status::deleted());
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Controller\Stations\Reports;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class RequestsAction
{
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
return $request->getView()->renderToResponse(
$response,
'stations/reports/requests',
[
'stationTz' => $station->getTimezone(),
]
);
}
}

View File

@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Stations\Reports;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Session\Flash;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
class RequestsController
{
protected string $csrf_namespace = 'stations_requests';
public function __construct(
protected EntityManagerInterface $em
) {
}
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
$requests = $this->em->createQuery(
<<<'DQL'
SELECT sr, sm
FROM App\Entity\StationRequest sr
JOIN sr.track sm
WHERE sr.station_id = :station_id
ORDER BY sr.timestamp DESC
DQL
)->setParameter('station_id', $station->getId())
->getArrayResult();
return $request->getView()->renderToResponse($response, 'stations/reports/requests', [
'requests' => $requests,
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
]);
}
public function deleteAction(
ServerRequest $request,
Response $response,
int $request_id,
string $csrf
): ResponseInterface {
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
$station = $request->getStation();
$media = $this->em->getRepository(Entity\StationRequest::class)->findOneBy([
'id' => $request_id,
'station_id' => $station->getId(),
'played_at' => 0,
]);
if ($media instanceof Entity\StationRequest) {
$this->em->remove($media);
$this->em->flush();
$request->getFlash()->addMessage('<b>Request deleted!</b>', Flash::SUCCESS);
}
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests'));
}
public function clearAction(
ServerRequest $request,
Response $response,
string $csrf
): ResponseInterface {
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
$station = $request->getStation();
$this->em->createQuery(
<<<'DQL'
DELETE FROM App\Entity\StationRequest sr
WHERE sr.station = :station
AND sr.played_at = 0
DQL
)->setParameter('station', $station)
->execute();
$request->getFlash()->addMessage('<b>All pending requests cleared.</b>', Flash::SUCCESS);
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests'));
}
}

View File

@ -10,6 +10,9 @@ use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use DateTimeInterface;
/**
* @extends Repository<Entity\Analytics>
*/
class AnalyticsRepository extends Repository
{
/**

View File

@ -4,6 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Repository;
use App\Entity;
/**
* @extends AbstractSplitTokenRepository<Entity\ApiKey>
*/
class ApiKeyRepository extends AbstractSplitTokenRepository
{
}

View File

@ -7,6 +7,9 @@ namespace App\Entity\Repository;
use App\Doctrine\Repository;
use App\Entity;
/**
* @extends Repository<Entity\CustomField>
*/
class CustomFieldRepository extends Repository
{
/**

View File

@ -10,6 +10,9 @@ use Carbon\CarbonImmutable;
use DateTimeInterface;
use NowPlaying\Result\Client;
/**
* @extends Repository<Entity\Listener>
*/
class ListenerRepository extends Repository
{
/**

View File

@ -6,10 +6,7 @@ namespace App\Entity\Repository;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Doctrine\Repository;
use App\Entity\Podcast;
use App\Entity\PodcastEpisode;
use App\Entity\Station;
use App\Entity\StorageLocation;
use App\Entity;
use App\Environment;
use Azura\Files\ExtendedFilesystemInterface;
use Intervention\Image\Constraint;
@ -18,6 +15,9 @@ use League\Flysystem\UnableToDeleteFile;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\PodcastEpisode>
*/
class PodcastEpisodeRepository extends Repository
{
public function __construct(
@ -30,7 +30,7 @@ class PodcastEpisodeRepository extends Repository
parent::__construct($entityManager, $serializer, $environment, $logger);
}
public function fetchEpisodeForStation(Station $station, string $episodeId): ?PodcastEpisode
public function fetchEpisodeForStation(Entity\Station $station, string $episodeId): ?Entity\PodcastEpisode
{
return $this->fetchEpisodeForStorageLocation(
$station->getPodcastsStorageLocation(),
@ -39,9 +39,9 @@ class PodcastEpisodeRepository extends Repository
}
public function fetchEpisodeForStorageLocation(
StorageLocation $storageLocation,
Entity\StorageLocation $storageLocation,
string $episodeId
): ?PodcastEpisode {
): ?Entity\PodcastEpisode {
return $this->em->createQuery(
<<<'DQL'
SELECT pe
@ -56,13 +56,13 @@ class PodcastEpisodeRepository extends Repository
}
/**
* @return PodcastEpisode[]
* @return Entity\PodcastEpisode[]
*/
public function fetchPublishedEpisodesForPodcast(Podcast $podcast): array
public function fetchPublishedEpisodesForPodcast(Entity\Podcast $podcast): array
{
$episodes = $this->em->createQueryBuilder()
->select('pe')
->from(PodcastEpisode::class, 'pe')
->from(Entity\PodcastEpisode::class, 'pe')
->where('pe.podcast = :podcast')
->setParameter('podcast', $podcast)
->getQuery()
@ -70,14 +70,14 @@ class PodcastEpisodeRepository extends Repository
return array_filter(
$episodes,
static function (PodcastEpisode $episode) {
static function (Entity\PodcastEpisode $episode) {
return $episode->isPublished();
}
);
}
public function writeEpisodeArt(
PodcastEpisode $episode,
Entity\PodcastEpisode $episode,
string $rawArtworkString
): void {
$episodeArtwork = $this->imageManager->make($rawArtworkString);
@ -89,7 +89,7 @@ class PodcastEpisodeRepository extends Repository
}
);
$episodeArtworkPath = PodcastEpisode::getArtPath($episode->getIdRequired());
$episodeArtworkPath = Entity\PodcastEpisode::getArtPath($episode->getIdRequired());
$episodeArtworkStream = $episodeArtwork->stream('jpg');
$fsPodcasts = $episode->getPodcast()->getStorageLocation()->getFilesystem();
@ -99,10 +99,10 @@ class PodcastEpisodeRepository extends Repository
}
public function removeEpisodeArt(
PodcastEpisode $episode,
Entity\PodcastEpisode $episode,
?ExtendedFilesystemInterface $fs = null
): void {
$artworkPath = PodcastEpisode::getArtPath($episode->getIdRequired());
$artworkPath = Entity\PodcastEpisode::getArtPath($episode->getIdRequired());
$fs ??= $episode->getPodcast()->getStorageLocation()->getFilesystem();
@ -115,7 +115,7 @@ class PodcastEpisodeRepository extends Repository
}
public function delete(
PodcastEpisode $episode,
Entity\PodcastEpisode $episode,
?ExtendedFilesystemInterface $fs = null
): void {
$fs ??= $episode->getPodcast()->getStorageLocation()->getFilesystem();

View File

@ -6,8 +6,7 @@ namespace App\Entity\Repository;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Doctrine\Repository;
use App\Entity\PodcastEpisode;
use App\Entity\PodcastMedia;
use App\Entity;
use App\Environment;
use App\Exception\InvalidPodcastMediaFileException;
use App\Media\MetadataManager;
@ -17,6 +16,9 @@ use League\Flysystem\UnableToDeleteFile;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\PodcastMedia>
*/
class PodcastMediaRepository extends Repository
{
public function __construct(
@ -32,7 +34,7 @@ class PodcastMediaRepository extends Repository
}
public function upload(
PodcastEpisode $episode,
Entity\PodcastEpisode $episode,
string $originalPath,
string $uploadPath,
?ExtendedFilesystemInterface $fs = null
@ -52,7 +54,7 @@ class PodcastMediaRepository extends Repository
}
$existingMedia = $episode->getMedia();
if ($existingMedia instanceof PodcastMedia) {
if ($existingMedia instanceof Entity\PodcastMedia) {
$this->delete($existingMedia, $fs);
$episode->setMedia(null);
}
@ -60,7 +62,7 @@ class PodcastMediaRepository extends Repository
$ext = pathinfo($originalPath, PATHINFO_EXTENSION);
$path = $podcast->getId() . '/' . $episode->getId() . '.' . $ext;
$podcastMedia = new PodcastMedia($storageLocation);
$podcastMedia = new Entity\PodcastMedia($storageLocation);
$podcastMedia->setPath($path);
$podcastMedia->setOriginalName(basename($originalPath));
@ -89,7 +91,7 @@ class PodcastMediaRepository extends Repository
}
public function delete(
PodcastMedia $media,
Entity\PodcastMedia $media,
?ExtendedFilesystemInterface $fs = null
): void {
$fs ??= $media->getStorageLocation()->getFilesystem();

View File

@ -13,6 +13,9 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @extends Repository<Entity\Settings>
*/
class SettingsRepository extends Repository
{
protected ValidatorInterface $validator;

View File

@ -12,6 +12,9 @@ use Carbon\CarbonImmutable;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\SongHistory>
*/
class SongHistoryRepository extends Repository
{
protected ListenerRepository $listenerRepository;

View File

@ -25,6 +25,9 @@ use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
/**
* @extends Repository<Entity\StationMedia>
*/
class StationMediaRepository extends Repository
{
public function __construct(

View File

@ -7,6 +7,9 @@ namespace App\Entity\Repository;
use App\Doctrine\Repository;
use App\Entity;
/**
* @extends Repository<Entity\StationPlaylistFolder>
*/
class StationPlaylistFolderRepository extends Repository
{
/**

View File

@ -17,6 +17,9 @@ use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\StationPlaylistMedia>
*/
class StationPlaylistMediaRepository extends Repository
{
protected StationQueueRepository $queueRepo;

View File

@ -11,6 +11,9 @@ use Carbon\CarbonInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
/**
* @extends Repository<Entity\StationQueue>
*/
class StationQueueRepository extends Repository
{
public function clearForMediaAndPlaylist(Entity\StationMedia $media, Entity\StationPlaylist $playlist): void

View File

@ -7,6 +7,9 @@ namespace App\Entity\Repository;
use App\Doctrine\Repository;
use App\Entity;
/**
* @extends Repository<Entity\StationRemote>
*/
class StationRemoteRepository extends Repository
{
/**

View File

@ -16,6 +16,9 @@ use Carbon\CarbonInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\StationRequest>
*/
class StationRequestRepository extends Repository
{
protected StationMediaRepository $mediaRepo;
@ -36,6 +39,29 @@ class StationRequestRepository extends Repository
$this->deviceDetector = $deviceDetector;
}
public function getPendingRequest(int $id, Entity\Station $station): ?Entity\StationRequest
{
return $this->repository->findOneBy(
[
'id' => $id,
'station' => $station,
'played_at' => 0,
]
);
}
public function clearPendingRequests(Entity\Station $station): void
{
$this->em->createQuery(
<<<'DQL'
DELETE FROM App\Entity\StationRequest sr
WHERE sr.station = :station
AND sr.played_at = 0
DQL
)->setParameter('station', $station)
->execute();
}
public function submit(
Entity\Station $station,
string $trackId,

View File

@ -14,6 +14,9 @@ use App\Radio\AutoDJ\Scheduler;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @extends Repository<Entity\StationStreamer>
*/
class StationStreamerRepository extends Repository
{
protected Scheduler $scheduler;

View File

@ -8,6 +8,9 @@ use App\Doctrine\Repository;
use App\Entity;
use Generator;
/**
* @extends Repository<Entity\UnprocessableMedia>
*/
class UnprocessableMediaRepository extends Repository
{
public function findByPath(string $path, Entity\StorageLocation $storageLocation): ?Entity\UnprocessableMedia

View File

@ -7,6 +7,9 @@ namespace App\Entity\Repository;
use App\Entity;
use App\Security\SplitToken;
/**
* @extends AbstractSplitTokenRepository<Entity\UserLoginToken>
*/
class UserLoginTokenRepository extends AbstractSplitTokenRepository
{
public function createToken(Entity\User $user): SplitToken

View File

@ -6,7 +6,6 @@ $this->layout('main', ['title' => __('Upcoming Song Queue'), 'manual' => true]);
$props = [
'listUrl' => (string)$router->fromHere('api:stations:queue'),
'clearUrl' => (string)$router->fromHere('api:stations:queue:clear'),
'locale' => substr($customization->getLocale(), 0, 2),
'stationTimeZone' => $stationTz,
];

View File

@ -1,12 +0,0 @@
$(function() {
var grid = $(".data-table").bootgrid({
caseSensitive: false,
sorting: false
}).on("loaded.rs.jquery.bootgrid", function() {
/* Executes after data is loaded and rendered */
grid.find("time[data-original]").each(function() {
$(this).text(moment.unix($(this).data('original')).format('lll'));
});
});
});

View File

@ -1,71 +1,16 @@
<?php $this->layout('main', ['title' => __('Song Requests'), 'manual' => true]) ?>
<?php
$this->layout('main', ['title' => __('Song Requests'), 'manual' => true]);
/** @var App\Http\RouterInterface $router */
$props = [
'listUrl' => (string)$router->fromHere('api:stations:reports:requests'),
'clearUrl' => (string)$router->fromHere('api:stations:reports:requests:clear'),
'stationTimeZone' => $stationTz,
];
/** @var \App\Assets $assets */
$assets
->load('bootgrid')
->load('moment')
->addInlineJs($this->fetch('stations/reports/requests.js'), 99);
$assets->addVueRender('Vue_StationsReportsRequests', '#station-report-requests', $props);
?>
<div class="card">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('Song Requests')?></h2>
</div>
<div class="card-actions">
<a class="btn btn-outline-danger" role="button" data-confirm-title="<?=$this->e(__('Clear all pending requests?'))?>"
href="<?=$router->fromHere('stations:reports:requests:clear', ['csrf' => $csrf])?>">
<i class="material-icons" aria-hidden="true">remove</i>
<?=__('Clear Pending Requests')?>
</a>
</div>
<table class="data-table table-responsive-md table table-striped mb-0">
<thead>
<tr>
<th style="width: 20%;" data-column-id="timestamp"><?=__('Date Requested')?></th>
<th style="width: 20%;" data-column-id="played_at"><?=__('Date Played')?></th>
<th style="width: 30%;" data-column-id="song"><?=__('Song Title')?></th>
<th style="width: 15%;" data-column-id="ip"><?=__('Requester IP')?></th>
<th style="width: 15%;" data-column-id="actions"><?=__('Actions')?></th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $request_row): ?>
<tr class="align-middle" id="request_<?=$request_row['id']?>">
<td>
<time data-original="<?=(int)$request_row['timestamp']?>"><?=date('F j, Y g:ia',
$request_row['timestamp'])?></time>
</td>
<td>
<?php if ($request_row['played_at'] > 0): ?>
<time data-original="<?=(int)$request_row['played_at']?>"><?=date('F j, Y g:ia',
$request_row['played_at'])?></time>
<?php else: ?>
<?=__('Not Played')?>
<?php endif; ?>
</td>
<td>
<?php if ($request_row['track']['title']): ?>
<b><?=$this->e($request_row['track']['title'])?></b><br>
<?=$this->e($request_row['track']['artist'])?>
<?php else: ?>
<?=$this->e($request_row['track']['text'])?>
<?php endif; ?>
</td>
<td><?=$this->e($request_row['ip'])?></td>
<td>
<?php if ($request_row['played_at'] == 0): ?>
<a class="btn btn-sm btn-danger" data-confirm-title="<?=$this->e(__('Delete request?'))?>"
href="<?=$router->fromHere('stations:reports:requests:delete',
['request_id' => $request_row['id'], 'csrf' => $csrf])?>">
<?=__('Delete')?>
</a>
<?php else: ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div id="station-report-requests"></div>