mirror of
https://github.com/openvk/openvk
synced 2024-11-14 02:59:12 +03:00
Add vouchers
Signed-off-by: Celestora <kitsuruko@gmail.com>
This commit is contained in:
parent
49f9d2b61b
commit
255d70e974
13 changed files with 393 additions and 18 deletions
|
@ -219,6 +219,11 @@ class User extends RowModel
|
|||
return $this->getRecord()->coins;
|
||||
}
|
||||
|
||||
function getRating(): int
|
||||
{
|
||||
return $this->getRecord()->rating;
|
||||
}
|
||||
|
||||
function getReputation(): int
|
||||
{
|
||||
return $this->getRecord()->reputation;
|
||||
|
@ -393,7 +398,8 @@ class User extends RowModel
|
|||
}
|
||||
|
||||
return (object) [
|
||||
"total" => 100 - $incompleteness,
|
||||
"total" => 100 - $incompleteness + $this->getRating(),
|
||||
"percent" => min(100 - $incompleteness + $this->getRating(), 100),
|
||||
"unfilled" => $unfilled,
|
||||
];
|
||||
}
|
||||
|
|
85
Web/Models/Entities/Voucher.php
Normal file
85
Web/Models/Entities/Voucher.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Entities;
|
||||
use Chandler\Database\DatabaseConnection as DB;
|
||||
use openvk\Web\Models\Repositories\Users;
|
||||
use openvk\Web\Models\RowModel;
|
||||
|
||||
class Voucher extends RowModel
|
||||
{
|
||||
protected $tableName = "coin_vouchers";
|
||||
|
||||
function getCoins(): int
|
||||
{
|
||||
return $this->getRecord()->coins;
|
||||
}
|
||||
|
||||
function getRating(): int
|
||||
{
|
||||
return $this->getRecord()->rating;
|
||||
}
|
||||
|
||||
function getToken(): string
|
||||
{
|
||||
return $this->getRecord()->token;
|
||||
}
|
||||
|
||||
function getFormattedToken(): string
|
||||
{
|
||||
$fmtTok = "";
|
||||
$token = $this->getRecord()->token;
|
||||
foreach(array_chunk(str_split($token), 6) as $chunk)
|
||||
$fmtTok .= implode("", $chunk) . "-";
|
||||
|
||||
return substr($fmtTok, 0, -1);
|
||||
}
|
||||
|
||||
function getRemainingUsages(): float
|
||||
{
|
||||
return (float) ($this->getRecord()->usages_left ?? INF);
|
||||
}
|
||||
|
||||
function getUsers(int $page = -1, ?int $perPage = NULL): \Traversable
|
||||
{
|
||||
$relations = $this->getRecord()->related("voucher_users.voucher");
|
||||
if($page !== -1)
|
||||
$relations = $relations->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
|
||||
|
||||
foreach($relations as $relation)
|
||||
yield (new Users)->get($relation->user);
|
||||
}
|
||||
|
||||
function isExpired(): bool
|
||||
{
|
||||
return $this->getRemainingUsages() < 1;
|
||||
}
|
||||
|
||||
function wasUsedBy(User $user): bool
|
||||
{
|
||||
$record = $this->getRecord()->related("voucher_users.voucher")->where("user", $user->getId());
|
||||
|
||||
return sizeof($record) > 0;
|
||||
}
|
||||
|
||||
function willUse(User $user): bool
|
||||
{
|
||||
if($this->wasUsedBy($user))
|
||||
return false;
|
||||
|
||||
if($this->isExpired())
|
||||
return false;
|
||||
|
||||
$this->setRemainingUsages($this->getRemainingUsages() - 1);
|
||||
DB::i()->getContext()->table("voucher_users")->insert([
|
||||
"voucher" => $this->getId(),
|
||||
"user" => $user->getId(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function setRemainingUsages(float $usages): void
|
||||
{
|
||||
$this->stateChanges("usages_left", $usages === INF ? NULL : ((int) $usages));
|
||||
$this->save();
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ use Nette\Database\Table\ActiveRow;
|
|||
|
||||
abstract class Repository
|
||||
{
|
||||
private $context;
|
||||
private $table;
|
||||
protected $context;
|
||||
protected $table;
|
||||
|
||||
protected $tableName;
|
||||
protected $modelName;
|
||||
|
@ -29,5 +29,18 @@ abstract class Repository
|
|||
return $this->toEntity($this->table->get($id));
|
||||
}
|
||||
|
||||
function size(bool $withDeleted = false): int
|
||||
{
|
||||
return sizeof($this->table->where("deleted", $withDeleted));
|
||||
}
|
||||
|
||||
function enumerate(int $page, ?int $perPage = NULL, bool $withDeleted = false): \Traversable
|
||||
{
|
||||
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
|
||||
|
||||
foreach($this->table->where("deleted", $withDeleted)->page($page, $perPage) as $entity)
|
||||
yield $this->toEntity($entity);
|
||||
}
|
||||
|
||||
use \Nette\SmartObject;
|
||||
}
|
||||
|
|
19
Web/Models/Repositories/Vouchers.php
Normal file
19
Web/Models/Repositories/Vouchers.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Repositories;
|
||||
use openvk\Web\Models\Entities\Voucher;
|
||||
|
||||
class Vouchers extends Repository
|
||||
{
|
||||
protected $tableName = "coin_vouchers";
|
||||
protected $modelName = "Voucher";
|
||||
|
||||
function getByToken(string $token, bool $withDeleted = false)
|
||||
{
|
||||
$voucher = $this->table->where([
|
||||
"token" => $token,
|
||||
"deleted" => $withDeleted,
|
||||
])->fetch();
|
||||
|
||||
return $this->toEntity($voucher);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use openvk\Web\Models\Entities\User;
|
||||
use openvk\Web\Models\Repositories\{Users, Clubs};
|
||||
use openvk\Web\Models\Entities\{Voucher, User};
|
||||
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers};
|
||||
|
||||
final class AdminPresenter extends OpenVKPresenter
|
||||
{
|
||||
private $users;
|
||||
private $clubs;
|
||||
private $vouchers;
|
||||
|
||||
function __construct(Users $users, Clubs $clubs)
|
||||
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers)
|
||||
{
|
||||
$this->users = $users;
|
||||
$this->clubs = $clubs;
|
||||
$this->vouchers = $vouchers;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -106,6 +108,57 @@ final class AdminPresenter extends OpenVKPresenter
|
|||
}
|
||||
}
|
||||
|
||||
function renderVouchers(): void
|
||||
{
|
||||
$this->template->count = $this->vouchers->size();
|
||||
$this->template->vouchers = iterator_to_array($this->vouchers->enumerate((int) ($this->queryParam("p") ?? 1)));
|
||||
}
|
||||
|
||||
function renderVoucher(int $id): void
|
||||
{
|
||||
$voucher = NULL;
|
||||
$this->template->form = (object) [];
|
||||
if($id === 0) {
|
||||
$this->template->form->id = 0;
|
||||
$this->template->form->token = NULL;
|
||||
$this->template->form->coins = 0;
|
||||
$this->template->form->rating = 0;
|
||||
$this->template->form->usages = -1;
|
||||
$this->template->form->users = [];
|
||||
} else {
|
||||
$voucher = $this->vouchers->get($id);
|
||||
if(!$voucher)
|
||||
$this->notFound();
|
||||
|
||||
$this->template->form->id = $voucher->getId();
|
||||
$this->template->form->token = $voucher->getToken();
|
||||
$this->template->form->coins = $voucher->getCoins();
|
||||
$this->template->form->rating = $voucher->getRating();
|
||||
$this->template->form->usages = $voucher->getRemainingUsages();
|
||||
$this->template->form->users = iterator_to_array($voucher->getUsers());
|
||||
|
||||
if($this->template->form->usages === INF)
|
||||
$this->template->form->usages = -1;
|
||||
else
|
||||
$this->template->form->usages = (int) $this->template->form->usages;
|
||||
}
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] !== "POST")
|
||||
return;
|
||||
|
||||
$voucher ??= new Voucher;
|
||||
$voucher->setCoins((int) $this->postParam("coins"));
|
||||
$voucher->setRating((int) $this->postParam("rating"));
|
||||
$voucher->setRemainingUsages($this->postParam("usages") === '-1' ? INF : ((int) $this->postParam("usages")));
|
||||
if(!empty($tok = $this->postParam("token")) && strlen($tok) === 24)
|
||||
$voucher->setToken($tok);
|
||||
|
||||
$voucher->save();
|
||||
|
||||
$this->redirect("/admin/vouchers/id" . $voucher->getId(), static::REDIRECT_TEMPORARY);
|
||||
exit;
|
||||
}
|
||||
|
||||
function renderFiles(): void
|
||||
{
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use openvk\Web\Models\Repositories\Users;
|
|||
use openvk\Web\Models\Repositories\Albums;
|
||||
use openvk\Web\Models\Repositories\Videos;
|
||||
use openvk\Web\Models\Repositories\Notes;
|
||||
use openvk\Web\Models\Repositories\Vouchers;
|
||||
|
||||
final class UserPresenter extends OpenVKPresenter
|
||||
{
|
||||
|
@ -240,7 +241,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
|
||||
if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc")))
|
||||
$this->flashFail("err", tr("error"), tr("error_shorturl_incorrect"));
|
||||
}elseif($_GET['act'] === "privacy") {
|
||||
} else if($_GET['act'] === "privacy") {
|
||||
$settings = [
|
||||
"page.read",
|
||||
"page.info.read",
|
||||
|
@ -256,7 +257,22 @@ final class UserPresenter extends OpenVKPresenter
|
|||
$input = $this->postParam(str_replace(".", "_", $setting));
|
||||
$user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting))));
|
||||
}
|
||||
}elseif($_GET['act'] === "interface") {
|
||||
} else if($_GET['act'] === "finance.top-up") {
|
||||
$token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3");
|
||||
$voucher = (new Vouchers)->getByToken($token);
|
||||
if(!$voucher)
|
||||
$this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad"));
|
||||
|
||||
$perm = $voucher->willUse($user);
|
||||
if(!$perm)
|
||||
$this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad"));
|
||||
|
||||
$user->setCoins($user->getCoins() + $voucher->getCoins());
|
||||
$user->setRating($user->getRating() + $voucher->getRating());
|
||||
$user->save();
|
||||
|
||||
$this->flashFail("succ", tr("voucher_good"), tr("voucher_redeemed"));
|
||||
} else if($_GET['act'] === "interface") {
|
||||
if (isset(Themepacks::i()[$this->postParam("style")]) || $this->postParam("style") === Themepacks::DEFAULT_THEME_ID)
|
||||
$user->setStyle($this->postParam("style"));
|
||||
|
||||
|
@ -271,7 +287,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
|
||||
if(in_array($this->postParam("nsfw"), [0, 1, 2]))
|
||||
$user->setNsfwTolerance((int) $this->postParam("nsfw"));
|
||||
}elseif($_GET['act'] === "lMenu") {
|
||||
} else if($_GET['act'] === "lMenu") {
|
||||
$settings = [
|
||||
"menu_bildoj" => "photos",
|
||||
"menu_filmetoj" => "videos",
|
||||
|
@ -300,7 +316,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
);
|
||||
}
|
||||
$this->template->mode = in_array($this->queryParam("act"), [
|
||||
"main", "privacy", "finance", "interface"
|
||||
"main", "privacy", "finance", "finance.top-up", "interface"
|
||||
]) ? $this->queryParam("act")
|
||||
: "main";
|
||||
$this->template->user = $user;
|
||||
|
|
|
@ -69,9 +69,14 @@
|
|||
Группы
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="aui-nav-heading">
|
||||
<strong>Платные услуги</strong>
|
||||
</div>
|
||||
<ul class="aui-nav">
|
||||
<li>
|
||||
<a href="/admin/files">
|
||||
Загруженные файлы
|
||||
<a href="/admin/vouchers">
|
||||
{_vouchers}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -141,5 +146,9 @@
|
|||
</section>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{script "js/node_modules/jquery/dist/jquery.min.js"}
|
||||
{script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"}
|
||||
<script>AJS.tabs.setup();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
92
Web/Presenters/templates/Admin/Voucher.xml
Normal file
92
Web/Presenters/templates/Admin/Voucher.xml
Normal file
|
@ -0,0 +1,92 @@
|
|||
{extends "@layout.xml"}
|
||||
|
||||
{block title}
|
||||
{_edit}
|
||||
{/block}
|
||||
|
||||
{block heading}
|
||||
{_edit} №{$form->token ?? "undefined"}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs">
|
||||
<ul class="tabs-menu">
|
||||
<li class="menu-item active-tab">
|
||||
<a href="#info">Информация</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="#activators">{_voucher_activators}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tabs-pane active-pane" id="info">
|
||||
<form class="aui" method="POST">
|
||||
<div class="field-group">
|
||||
<label for="id">
|
||||
ID
|
||||
</label>
|
||||
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="token">
|
||||
Серийный номер
|
||||
</label>
|
||||
<input class="text long-field" type="text" id="token" name="token" value="{$form->token}" />
|
||||
<div class="description">Номер состоит из 24 символов, если формат неправильный или поле не заполнено, будет назначен автоматически.</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="coins">
|
||||
Количество голосов
|
||||
</label>
|
||||
<input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="rating">
|
||||
Количество рейтинга
|
||||
</label>
|
||||
<input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="usages">
|
||||
{if $form->id === 0}
|
||||
{_usages_total}
|
||||
{else}
|
||||
{_usages_left}
|
||||
{/if}
|
||||
</label>
|
||||
<input class="text long-field" type="number" min="-1" id="usages" name="usages" value="{$form->usages}" />
|
||||
<div class="description">Количество аккаунтов, которые могут использовать ваучер. Если написать -1, будет Infinity.</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-container">
|
||||
<div class="buttons">
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tabs-pane" id="activators">
|
||||
<table rules="none" class="aui aui-table-list">
|
||||
<tbody>
|
||||
<tr n:foreach="$form->users as $user">
|
||||
<td>
|
||||
<span class="aui-avatar aui-avatar-xsmall">
|
||||
<span class="aui-avatar-inner">
|
||||
<img src="{$user->getAvatarUrl()}" alt="{$user->getCanonicalName()}" role="presentation" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
|
||||
|
||||
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">
|
||||
заблокирован
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
61
Web/Presenters/templates/Admin/Vouchers.xml
Normal file
61
Web/Presenters/templates/Admin/Vouchers.xml
Normal file
|
@ -0,0 +1,61 @@
|
|||
{extends "@layout.xml"}
|
||||
|
||||
{block title}
|
||||
{_vouchers}
|
||||
{/block}
|
||||
|
||||
{block heading}
|
||||
{_vouchers}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<table class="aui aui-table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Серийный номер</th>
|
||||
<th>Голоса</th>
|
||||
<th>Рейгтинг</th>
|
||||
<th>Осталось использований</th>
|
||||
<th>Состояние</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr n:foreach="$vouchers as $voucher">
|
||||
<td>{$voucher->getId()}</td>
|
||||
<td>{$voucher->getFormattedToken()}</td>
|
||||
<td>{$voucher->getCoins()}¢</td>
|
||||
<td>{$voucher->getRating()}</td>
|
||||
<td>{$voucher->getRemainingUsages() === INF ? "∞" : $voucher->getRemainingUsages()}</td>
|
||||
<td>
|
||||
{if $voucher->isExpired()}
|
||||
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">закончился</span>
|
||||
{else}
|
||||
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">активен</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a class="aui-button aui-button-primary" href="/admin/vouchers/id{$voucher->getId()}">
|
||||
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
<div align="right">
|
||||
<a style="float: left;" class="aui-button aui-button-primary" href="/admin/vouchers/id0">
|
||||
{_create}
|
||||
</a>
|
||||
|
||||
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count}
|
||||
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
|
||||
⭁ туда
|
||||
</a>
|
||||
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
|
||||
⭇ сюда
|
||||
</a>
|
||||
</div>
|
||||
{/block}
|
|
@ -10,6 +10,7 @@
|
|||
{var isMain = $mode === 'main'}
|
||||
{var isPrivacy = $mode === 'privacy'}
|
||||
{var isFinance = $mode === 'finance'}
|
||||
{var isFinanceTU = $mode === 'finance.top-up'}
|
||||
{var isInterface = $mode === 'interface'}
|
||||
|
||||
<div class="tabs">
|
||||
|
@ -19,8 +20,8 @@
|
|||
<div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_"privacy"}</a>
|
||||
</div>
|
||||
<div n:attr="id => ($isFinance ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($isFinance ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a>
|
||||
<div n:attr="id => (($isFinance || $isFinanceTU) ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => (($isFinance || $isFinanceTU) ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a>
|
||||
</div>
|
||||
<div n:attr="id => ($isInterface ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($isInterface ? 'act_tab_a' : 'ki')" href="/settings?act=interface">{_"interface"}</a>
|
||||
|
@ -265,11 +266,26 @@
|
|||
<b>
|
||||
{_on_your_account}<br/>
|
||||
<span style="font-size: 50px;">{$thisUser->getCoins()}</span><br/>
|
||||
{_points_count}
|
||||
{_points_count}<br/><br/>
|
||||
<small><a href="?act=finance.top-up">[{_have_voucher}?]</a></small>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{elseif $isFinanceTU}
|
||||
|
||||
<p>{_voucher_explanation} {_voucher_explanation_ex}</p>
|
||||
<form action="/settings?act=finance.top-up" method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="key0" size="6" placeholder="123456" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
|
||||
<input type="text" name="key1" size="6" placeholder="789012" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
|
||||
<input type="text" name="key2" size="6" placeholder="345678" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
|
||||
<input type="text" name="key3" size="6" placeholder="90ABCD" required="required" style="display: inline-block; width: 50px; text-align: center;" />
|
||||
<br/><br/>
|
||||
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input type="submit" value="{_redeem}" class="button" />
|
||||
</form>
|
||||
|
||||
{elseif $isInterface}
|
||||
|
||||
<h4>{_ui_settings_interface}</h4>
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
{var completeness = $user->getProfileCompletenessReport()}
|
||||
|
||||
<div class="completeness-gauge">
|
||||
<div style="width: {$completeness->total}%"></div>
|
||||
<div style="width: {$completeness->percent}%"></div>
|
||||
<span>{$completeness->total}%</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -33,4 +33,5 @@ services:
|
|||
- openvk\Web\Models\Repositories\Notifications
|
||||
- openvk\Web\Models\Repositories\TicketComments
|
||||
- openvk\Web\Models\Repositories\IPs
|
||||
- openvk\Web\Models\Repositories\Vouchers
|
||||
- openvk\Web\Models\Repositories\ContentSearchRepository
|
||||
|
|
|
@ -209,6 +209,10 @@ routes:
|
|||
handler: "Admin->clubs"
|
||||
- url: "/admin/clubs/id{num}"
|
||||
handler: "Admin->club"
|
||||
- url: "/admin/vouchers"
|
||||
handler: "Admin->vouchers"
|
||||
- url: "/admin/vouchers/id{num}"
|
||||
handler: "Admin->voucher"
|
||||
- url: "/admin/ban.pl/{num}"
|
||||
handler: "Admin->quickBan"
|
||||
- url: "/admin/warn.pl/{num}"
|
||||
|
|
Loading…
Reference in a new issue