FAQ в Поддержке

This commit is contained in:
n1rwana 2023-07-24 14:30:38 +03:00
parent a2384cc231
commit 047c0922fc
16 changed files with 780 additions and 2 deletions

View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\FAQCategories;
use openvk\Web\Models\RowModel;
class FAQArticle extends RowModel
{
protected $tableName = "faq_articles";
function getId(): int
{
return $this->getRecord()->id;
}
function getTitle(): string
{
return $this->getRecord()->title;
}
function getText(): string
{
return $this->getRecord()->text;
}
function canSeeByUnloggedUsers(): bool
{
return (bool) $this->getRecord()->unlogged_can_see;
}
function canSeeByUsers(): bool
{
return (bool) $this->getRecord()->users_can_see;
}
function getCategory(): ?FAQCategory
{
return (new FAQCategories)->get($this->getRecord()->category);
}
function getLanguage(): string
{
return $this->getRecord()->language;
}
}

View file

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use Chandler\Database\DatabaseConnection;
class FAQCategory extends RowModel
{
protected $tableName = "faq_categories";
function getId(): int
{
return $this->getRecord()->id;
}
function getTitle(): string
{
return $this->getRecord()->title;
}
function canSeeByUsers(): bool
{
return (bool) !$this->getRecord()->for_agents_only;
}
function getIconBackgroundPosition(): int
{
return 28 * $this->getRecord()->icon;
}
function getArticles(?int $limit = NULL, $isAgent): \Traversable
{
$filter = ["category" => $this->getId(), "deleted" => 0];
if (!$isAgent) $filter["users_can_see"] = 1;
$articles = DatabaseConnection::i()->getContext()->table("faq_articles")->where($filter)->limit($limit);
foreach ($articles as $article) {
yield new FAQArticle($article);
}
}
function getIcon(): int
{
return $this->getRecord()->icon;
}
function getLanguage(): string
{
return $this->getRecord()->language;
}
}

View file

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Entities\FAQArticle;
class FAQArticles
{
private $context;
private $articles;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->articles = $this->context->table("faq_articles");
}
function toFAQArticle(?ActiveRow $ar): ?FAQArticle
{
return is_null($ar) ? NULL : new FAQArticle($ar);
}
function get(int $id): ?FAQArticle
{
return $this->toFAQArticle($this->articles->get($id));
}
}

View file

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Entities\FAQCategory;
class FAQCategories
{
private $context;
private $categories;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->categories = $this->context->table("faq_categories");
}
function toFAQCategory(?ActiveRow $ar): ?FAQCategory
{
return is_null($ar) ? NULL : new FAQCategory($ar);
}
function get(int $id): ?FAQCategory
{
return $this->toFAQCategory($this->categories->get($id));
}
function getList(string $language, bool $includeForAgents = false): \Traversable
{
$filter = ["deleted" => 0, "language" => $language];
if (!$includeForAgents) $filter["for_agents_only"] = 0;
foreach ($this->categories->where($filter) as $category) {
yield new FAQCategory($category);
}
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{SupportAgent, Ticket, TicketComment};
use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments, SupportAgents};
use openvk\Web\Models\Entities\{FAQArticle, FAQCategory, SupportAgent, Ticket, TicketComment};
use openvk\Web\Models\Repositories\{FAQCategories, Tickets, Users, TicketComments, SupportAgents, FAQArticles};
use openvk\Web\Util\Telegram;
use Chandler\Session\Session;
use Chandler\Database\DatabaseConnection;
@ -28,8 +28,12 @@ final class SupportPresenter extends OpenVKPresenter
{
$this->assertUserLoggedIn();
$this->template->mode = in_array($this->queryParam("act"), ["faq", "new", "list"]) ? $this->queryParam("act") : "faq";
$canEdit = $this->user->identity->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0);
if($this->template->mode === "faq") {
$this->template->categories = (new FAQCategories)->getList($canEdit ? $this->queryParam("lang") ?? Session::i()->get("lang", "ru") : Session::i()->get("lang", "ru"), $canEdit);
$this->template->canEditFAQ = $canEdit;
$lang = Session::i()->get("lang", "ru");
$base = OPENVK_ROOT . "/data/knowledgebase/faq";
if(file_exists("$base.$lang.md"))
@ -104,6 +108,9 @@ final class SupportPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("you_have_not_entered_name_or_text"));
}
}
$this->template->languages = getLanguages();
$this->template->activeLang = $this->queryParam("lang") ?? Session::i()->get("lang", "ru");
}
function renderList(): void
@ -396,4 +403,191 @@ final class SupportPresenter extends OpenVKPresenter
$this->flashFail("succ", "Успех", "Профиль создан. Теперь пользователи видят Ваши псевдоним и аватарку вместо стандартных аватарки и номера.");
}
}
function renderFAQArticle(int $id): void
{
$article = (new FAQArticles)->get($id);
if (!$article || $article->isDeleted())
$this->notFound();
$category = $article->getCategory();
if ($category->isDeleted())
$this->notFound();
if (!$article->canSeeByUnloggedUsers())
$this->assertUserLoggedIn();
if (!$category->canSeeByUsers() || (!$article->canSeeByUsers() && !$article->canSeeByUnloggedUsers()))
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$canEdit = false;
if ($this->user->identity)
$canEdit = $this->user->identity->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
if (!$canEdit) {
$this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
}
$cid = $this->postParam("category");
if ($this->postParam("language") != $article->getLanguage()) {
$categories = iterator_to_array((new FAQCategories)->getList($this->postParam("language")));
if (sizeof($categories) > 0) {
$cid = iterator_to_array((new FAQCategories)->getList($this->postParam("language")))[0]->getId();
} else {
$this->flashFail("err", tr("error"), tr("support_cant_change_lang_no_cats"));
}
}
$article->setTitle($this->postParam("title"));
$article->setText($this->postParam("text"));
$article->setUnlogged_Can_see(empty($this->postParam("unlogged_can_see") ? 0 : 1));
$article->setUsers_Can_See(empty($this->postParam("users_can_see") ? 0 : 1));
$article->setCategory($cid);
$article->setLanguage($this->postParam("language"));
$article->save();
$this->flashFail("succ", tr("changes_saved"));
} else {
$this->template->mode = $canEdit ? in_array($this->queryParam("act"), ["view", "edit"]) ? $this->queryParam("act") : "view" : "view";
$this->template->category = $category;
$this->template->article = $article;
$this->template->text = (new Parsedown())->text($article->getText());
$this->template->canEditFAQ = $canEdit;
$this->template->categories = (new FAQCategories)->getList($this->queryParam("lang") ?? $article->getLanguage(), TRUE);
$this->template->languages = getLanguages();
$this->template->activeLang = $this->queryParam("lang") ?? $article->getLanguage();
}
}
function renderFAQCategory(int $id): void
{
$category = (new FAQCategories)->get($id);
if (!$category)
$this->notFound();
if (!$category->canSeeByUsers())
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$canEdit = $this->user->identity->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
if (!$canEdit) {
$this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
}
if ($this->queryParam("mode") === "copy") {
$orig_cat = $category;
$category = new FAQCategory;
}
$category->setTitle($this->postParam("title"));
$category->setIcon($orig_cat->getIcon() ?? $this->postParam("icon"));
$category->setFor_Agents_Only(empty($this->postParam("for_agents_only") ? 0 : 1));
$category->setLanguage($this->postParam("language"));
$category->save();
if ($this->queryParam("mode") === "copy" && !empty($this->postParam("copy_with_articles"))) {
$articles = $orig_cat->getArticles(NULL, TRUE);
foreach ($articles as $article) {
$_article = new FAQArticle;
$_article->setCategory($category->getId());
$_article->setTitle($article->getTitle());
$_article->setText($article->getText());
$_article->setUsers_Can_See($article->canSeeByUsers());
$_article->setUnlogged_Can_See($article->canSeeByUnloggedUsers());
$_article->setLanguage($category->getLanguage());
$_article->save();
}
}
$this->flashFail("succ", tr("changes_saved"));
}
$this->template->mode = $canEdit ? in_array($this->queryParam("act"), ["view", "edit"]) ? $this->queryParam("act") : "view" : "view";
$this->template->copyMode = $canEdit ? $this->queryParam("mode") === "copy" : FALSE;
$this->template->category = $category;
$this->template->canEditFAQ = $canEdit;
$this->template->languages = getLanguages();
}
function renderFAQNewArticle(): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!$this->postParam("category"))
$this->flashFail("err", tr("support_category_not_specified"));
$article = new FAQArticle;
$article->setTitle($this->postParam("title"));
$article->setText($this->postParam("text"));
$article->setUnlogged_Can_see(empty($this->postParam("unlogged_can_see") ? 0 : 1));
$article->setUsers_Can_See(empty($this->postParam("users_can_see") ? 0 : 1));
$article->setCategory($this->postParam("category"));
$article->setLanguage(Session::i()->get("lang", "ru"));
$article->save();
$this->redirect("/faq" . $article->getId());
} else {
$this->template->categories = (new FAQCategories)->getList($this->queryParam("lang") ?? Session::i()->get("lang", "ru"), TRUE);
$this->template->category_id = $this->queryParam("cid");
$this->template->activeLang = $this->queryParam("lang") ?? Session::i()->get("lang", "ru");
$this->template->languages = getLanguages();
}
}
function renderFAQNewCategory(): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$category = new FAQCategory;
$category->setTitle($this->postParam("title"));
$category->setIcon($this->postParam("icon"));
$category->setFor_Agents_Only(empty($this->postParam("for_agents_only") ? 0 : 1));
$category->setLanguage($this->postParam("language"));
$category->save();
$this->redirect("/faqs" . $category->getId());
}
$this->template->activeLang = $this->queryParam("lang") ?? Session::i()->get("lang", "ru");
$this->template->languages = getLanguages();
}
function renderFAQDeleteArticle(int $id): void
{
$this->assertNoCSRF();
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$article = (new FAQArticles)->get($id);
if (!$article || $article->isDeleted())
$this->notFound();
$cid = $article->getCategory()->getId();
$article->delete();
$this->redirect("/faqs" . $cid);
}
function renderFAQDeleteCategory(int $id): void
{
$this->assertNoCSRF();
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$category = (new FAQCategories)->get($id);
if (!$category || $category->isDeleted())
$this->notFound();
if (!empty($this->postParam("delete_articles"))) {
$articles = $category->getArticles(NULL, TRUE);
foreach ($articles as $article) {
$article->delete();
}
}
$category->delete();
$this->redirect("/support");
}
}

View file

@ -0,0 +1,73 @@
{extends "../@layout.xml"}
{block title}{$title}{/block}
{block header}
<a href="/support">{_menu_help}</a>
> <a href="/faqs{$category->getId()}">{$category->getTitle()}</a>
> <a n:if="$mode === 'edit'" href="/faq{$article->getId()}">{$article->getTitle()}</a>
<div style="display:inline;" n:if="$mode === 'view'">{_support_article} <a n:if="$canEditFAQ" href="/faq{$article->getId()}?act=edit">({_app_edit})</a></div>
<div style="display:inline;" n:if="$mode === 'edit'">> {_support_edit_mode}</div>
{/block}
{block content}
<script>
function onLanguageSelectChanged(e) {
window.location.href = "/faq{$article->getId()}?act=edit&lang=" + e.value;
}
</script>
<div n:if="$mode === 'view'">
<h4 style="padding:8px 0;">{$article->getTitle()}</h4>
{$text|noescape}
</div>
<div n:if="$mode === 'edit'">
<form action="/faq{$article->getId()}" method="post" style="margin:0;">
<center>
<input value="{$article->getTitle()}" type="text" name="title" style="width: 80%; resize: vertical;" placeholder="{_support_faq_title_placeholder}" /><br /><br />
<textarea name="text" style="width: 80%; resize: vertical; height: 200px" placeholder="{_support_faq_text_placeholder}">{$article->getText()}</textarea><br /><br />
<input n:attr="checked => $article->canSeeByUnloggedUsers()" type="checkbox" name="unlogged_can_see" value="1" /> {_support_unlogged_can_see}
<input n:attr="checked => $article->canSeeByUsers()" type="checkbox" name="users_can_see" value="1" /> {_support_users_can_see}
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_category}: </span>
</td>
<td>
<select name="category">
<option
n:attr="selected => $article->getCategory()->getId() === $category->getId()"
n:foreach="$categories as $category"
value="{$category->getId()}"
>[#{$category->getId()}] {$category->getTitle()}</option>
</select>
</td>
</tr>
</table>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_language}: </span>
</td>
<td>
<select name="language" onchange="onLanguageSelectChanged(this)">
<option
n:attr="selected => $language['code'] === $activeLang"
n:foreach="$languages as $language"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
</td>
</tr>
</table>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" style="margin-left: 70%;" /><br /><br />
</center>
</form>
<h4 /><br />
<form action="/al_helpdesk/{$article->getId()}/delete" method="post" style="margin:0;">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_delete}" class="button" />
</form>
</div>
{/block}

View file

@ -0,0 +1,90 @@
{extends "../@layout.xml"}
{block title}{$title}{/block}
{block header}
<a href="/support">{_menu_help}</a>
<div style="display:inline;" n:if="$mode === 'view'">> {_support_faq_category}</div>
<div style="display:inline;" n:if="$mode === 'edit'">> <a href="/faqs{$category->getId()}">{$category->getTitle()}</a> > {_support_edit_mode}</div>
{/block}
{block content}
{var $articles = iterator_to_array($category->getArticles(NULL, $canEditFAQ))}
<div n:if="$mode === 'view'">
<h4 style="display:flex;line-height:28px;gap:8px;padding:8px;">
<div style="width:28px;height:28px;border-radius:3px;background:url('/assets/packages/static/openvk/img/faq_icons.png');background-position:0 -{$category->getIconBackgroundPosition()}px;"/>
{$category->getTitle()}
<div n:if="$canEditFAQ">
<a href="/faqs{$category->getId()}?act=edit">({_support_edit})</a>
<a href="/faqs{$category->getId()}?act=edit&mode=copy">({_support_copy})</a>
<a href="/new_faq?lang={$category->getLanguage()}&cid={$category->getId()}">(+{_support_add_article})</a>
</div>
</h4>
<div style="margin:14px;" />
<ul n:if="count($articles) > 0" n:foreach="$articles as $article" style="padding-inline-start:18px;">
<li>
<a href="/faq{$article->getId()}" style="display:block;">
<h4 style="display:inherit;padding:8px;">{$article->getTitle()}</h4>
</a>
</li>
</ul>
<center n:if="count($articles) === 0" style="padding: 30px;">
<img src="/assets/packages/static/openvk/img/sad.png" height="45"/>
<div style="margin-top: 8px;">{_support_empty}</div>
</center>
</div>
<div n:if="$mode === 'edit'">
<form method="post" style="margin:0;">
<center>
<input value="{$category->getTitle()}" type="text" name="title" style="width: 80%; resize: vertical;" placeholder="{_support_faq_title_placeholder}" /><br /><br />
<input n:attr="checked => !$category->canSeeByUsers()" type="checkbox" name="for_agents_only" value="1" /> {_support_for_agents_only}
<span n:if="$copyMode"><input type="checkbox" name="copy_with_articles" value="1" /> {_support_copy_articles}</span>
<br/><br/>
<div style="display:grid;grid-template-columns: repeat(11, auto);row-gap: 10px;">
<div n:for="$i = 0; $i < 22; $i++" onclick="onIconClick({$i})">
<div id="icon-{$i}" style="width:28px;height:28px;border-radius:3px;background:url('/assets/packages/static/openvk/img/faq_icons.png');background-position:0 -{28 * $i}px;cursor:pointer;" />
</div>
</div>
<br/>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_language}: </span>
</td>
<td>
<select name="language">
<option
n:attr="selected => $language['code'] === $category->getLanguage()"
n:foreach="$languages as $language"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
</td>
</tr>
</table>
<input id="icon-number" type="hidden" name="icon" value="0" />
<div style="display:none;" id="prev-icon">0</div>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" style="margin-left: 70%;" /><br /><br />
</center>
</form>
<h4 /><br />
<form action="/al_helpdesk_cat/{$category->getId()}/delete" method="post" style="margin:0;">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_delete}" class="button" />
<input type="checkbox" name="delete_articles" value="1" /> {_support_delete_articles}
</form>
</div>
<script>
const icon_number_elem = document.getElementById("icon-number");
const prev_icon = document.getElementById("prev-icon");
function onIconClick(number) {
icon_number_elem.value = number;
document.getElementById("icon-" + prev_icon.innerHTML).style.border = "none";
prev_icon.innerHTML = number;
document.getElementById("icon-" + number).style.border = "3px solid red";
}
</script>
{/block}

View file

@ -0,0 +1,57 @@
{extends "../@layout.xml"}
{block title}{_support_new_article}{/block}
{block header}<a href="/support?lang={$activeLang}">{_menu_help}</a> > {_support_create_article}{/block}
{block content}
<script>
function onLanguageSelectChanged(e) {
window.location.href = "/new_faq?lang=" + e.value;
}
</script>
<form action="/new_faq" method="post" style="margin:0;">
<center>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_language}: </span>
</td>
<td>
<select name="language" onchange="onLanguageSelectChanged(this)">
<option
n:attr="selected => $language['code'] == $activeLang"
n:foreach="$languages as $language"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
</td>
</tr>
</table>
<input type="text" name="title" style="width: 80%; resize: vertical;" placeholder="{_support_faq_title_placeholder}" /><br /><br />
<textarea name="text" style="width: 80%; resize: vertical;" placeholder="{_support_faq_text_placeholder}"></textarea><br /><br />
<input type="checkbox" name="unlogged_can_see" value="1" /> {_support_unlogged_can_see}
<input type="checkbox" name="users_can_see" value="1" /> {_support_users_can_see}
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_category}: </span>
</td>
<td>
<select name="category">
<option
n:attr="selected => $category->getId() == $category_id"
n:foreach="$categories as $category"
value="{$category->getId()}"
>[#{$category->getId()}] {$category->getTitle()}</option>
</select>
</td>
</tr>
</table>
{$test}
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_create}" class="button" style="margin-left: 70%;" /><br /><br />
</center>
</form>
{/block}

View file

@ -0,0 +1,56 @@
{extends "../@layout.xml"}
{block title}{_support_new_category}{/block}
{block header}<a href="/support?lang={$activeLang}">{_menu_help}</a> > {_support_create_category}{/block}
{block content}
<form action="/new_faq_cat" method="post" style="margin:0;">
<center>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_language}: </span>
</td>
<td>
<select name="language">
<option
n:attr="selected => $language['code'] == $activeLang"
n:foreach="$languages as $language"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
</td>
</tr>
</table>
<input type="text" name="title" style="width: 80%; resize: vertical;" placeholder="{_support_faq_title_placeholder}" /><br /><br />
<input type="checkbox" name="for_agents_only" value="1" /> {_support_for_agents_only}
<br/><br/>
</center>
<b style="display:block;margin-bottom:8px;padding:0 16px;">Иконка</b>
<center>
<div style="display:grid;grid-template-columns: repeat(11, auto);row-gap: 10px">
<div n:for="$i = 0; $i < 22; $i++" onclick="onIconClick({$i})">
<div id="icon-{$i}" style="width:28px;height:28px;border-radius:3px;background:url('/assets/packages/static/openvk/img/faq_icons.png');background-position:0 -{28 * $i}px;cursor:pointer;" />
</div>
</div>
<br/><br/>
<input id="icon-number" type="hidden" name="icon" value="0" />
<div style="display:none;" id="prev-icon">0</div>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_create}" class="button" style="margin-left: 70%;" /><br /><br />
</center>
</form>
<script>
const icon_number_elem = document.getElementById("icon-number");
const prev_icon = document.getElementById("prev-icon");
function onIconClick(number) {
icon_number_elem.value = number;
document.getElementById("icon-" + prev_icon.innerHTML).style.border = "none";
prev_icon.innerHTML = number;
document.getElementById("icon-" + number).style.border = "3px solid red";
}
</script>
{/block}

View file

@ -49,7 +49,57 @@
{/if}
{if $isMain}
<script>
function onLanguageSelectChanged(e) {
window.location.href = "/support?lang=" + e.value;
}
</script>
<h4>{_support_faq}</h4><br />
<div n:if="$canEditFAQ" style="display:flex;justify-content:space-between;">
<div>
{_create}
<a href="/new_faq_cat?lang={$activeLang}">{_support_category_acc}</a>
·
<a href="/new_faq?lang={$activeLang}">{_support_article_acc}</a>
</div>
<table cellspacing="7" cellpadding="0" border="0">
<tr>
<td width="120" valign="top">
<span class="nobold">{_support_language}: </span>
</td>
<td>
<select name="language" onchange="onLanguageSelectChanged(this)">
<option
n:attr="selected => $language['code'] === $activeLang"
n:foreach="$languages as $language"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
</td>
</tr>
</table>
</div>
<div style="display:grid;grid-template-columns: repeat(2, 1fr);row-gap:16px;">
<div n:foreach="$categories as $category" href="/faqs{$category->getId()}">
<h4 style="display:flex;line-height:28px;gap:8px;padding:8px;max-width:85%;">
<div style="width:28px;height:28px;border-radius:3px;background:url('/assets/packages/static/openvk/img/faq_icons.png');background-position:0 -{$category->getIconBackgroundPosition()}px;"/>
<a href="/faqs{$category->getId()}">{$category->getTitle()}</a>
</h4>
{var $articles = iterator_to_array($category->getArticles(3, $canEditFAQ))}
<ul n:if="count($articles) > 0" style="padding-inline-start:18px;">
<li n:foreach="$articles as $article">
<a href="/faq{$article->getId()}" style="display:block;">
<h4 style="display:inherit;padding:8px;border:none;">{$article->getTitle()}</h4>
</a>
</li>
</ul>
<center n:if="count($articles) === 0" style="padding: 30px;">
<img src="/assets/packages/static/openvk/img/sad.png" height="45"/>
<div style="margin-top: 8px;">{_support_empty}</div>
</center>
</div>
</div>
<br/><br/>
<div n:foreach="$faq as $section" class="faq">
<div id="faqhead">{$section[0]}</div>
<div id="faqcontent">{$section[1]|noescape}</div>

View file

@ -43,6 +43,18 @@ routes:
handler: "About->donate"
- url: "/kb/{slug}"
handler: "Support->knowledgeBaseArticle"
- url: "/faq{num}"
handler: "Support->FAQArticle"
- url: "/faqs{num}"
handler: "Support->FAQCategory"
- url: "/new_faq"
handler: "Support->FAQNewArticle"
- url: "/new_faq_cat"
handler: "Support->FAQNewCategory"
- url: "/al_helpdesk/{num}/delete"
handler: "Support->FAQDeleteArticle"
- url: "/al_helpdesk_cat/{num}/delete"
handler: "Support->FAQDeleteCategory"
- url: "/about:{?!productName}"
handler: "About->version"
placeholders:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
Web/static/img/sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,37 @@
CREATE TABLE `faq_categories`
(
`id` bigint(20) UNSIGNED NOT NULL,
`title` tinytext NOT NULL,
`for_agents_only` tinyint(1) DEFAULT NULL,
`icon` int(11) NOT NULL,
`language` varchar(255) NOT NULL,
`deleted` tinyint(1) DEFAULT 0
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
ALTER TABLE `faq_categories`
ADD PRIMARY KEY (`id`);
ALTER TABLE `faq_categories`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;
CREATE TABLE `faq_articles`
(
`id` bigint(20) UNSIGNED NOT NULL,
`category` bigint(20) UNSIGNED DEFAULT NULL,
`title` mediumtext NOT NULL,
`text` longtext NOT NULL,
`users_can_see` tinyint(1) DEFAULT NULL,
`unlogged_can_see` tinyint(1) DEFAULT NULL,
`language` varchar(255) NOT NULL,
`deleted` tinyint(1) DEFAULT 0
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
ALTER TABLE `faq_articles`
ADD PRIMARY KEY (`id`);
ALTER TABLE `faq_articles`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -913,6 +913,32 @@
"banned_in_support_1" = "Sorry, <b>$1</b>, but now you can't create tickets.";
"banned_in_support_2" = "And the reason for this is simple: <b>$1</b>. Unfortunately, this time we had to take away this opportunity from you forever.";
"support_article" = "Article";
"support_edit_mode" = "Editing";
"support_faq_title_placeholder" = "A long time ago,";
"support_faq_text_placeholder" = "In a galaxy far, far away...";
"support_unlogged_can_see" = "Unauthorized users can see";
"support_users_can_see" = "Users can see";
"support_category" = "Category";
"support_faq_category" = "FAQ Category";
"support_edit" = "edit";
"support_add_article" = "article";
"support_empty" = "There's nothing here yet";
"support_for_agents_only" = "Only for agents";
"support_icon_number" = "Icon number";
"support_create_article" = "Create an article";
"support_create_category" = "Create a category";
"support_new_article" = "New article";
"support_new_category" = "New category";
"support_article_acc" = "an article";
"support_category_acc" = "a category";
"support_language" = "Language";
"support_category_not_specified" = "You didn't specify a category";
"support_cant_change_lang_no_cats" = "In this localization, not a single category has been created yet to which the article could be moved";
"support_copy" = "copy";
"support_delete_articles" = "Delete articles";
"support_copy_articles" = "Copy with articles";
/* Invite */
"invite" = "Invite";

View file

@ -841,6 +841,31 @@
"ticket_changed_comment" = "Изменения вступят силу через несколько секунд.";
"banned_in_support_1" = "Извините, <b>$1</b>, но теперь вам нельзя создавать обращения.";
"banned_in_support_2" = "А причина этому проста: <b>$1</b>. К сожалению, на этот раз нам пришлось отобрать у вас эту возможность навсегда.";
"support_article" = "Статья";
"support_edit_mode" = "Редактирование";
"support_faq_title_placeholder" = "Давным-давно,";
"support_faq_text_placeholder" = "В далёкой-далёкой галактике...";
"support_unlogged_can_see" = "Могут видеть неавторизованные";
"support_users_can_see" = "Могут видеть пользователи";
"support_category" = "Категория";
"support_faq_category" = "Категория FAQ";
"support_edit" = "редактировать";
"support_add_article" = "статья";
"support_empty" = "Здесь пока ничего нет";
"support_for_agents_only" = "Только для агентов";
"support_icon_number" = "Номер иконки";
"support_create_article" = "Создать статью";
"support_create_category" = "Создать категорию";
"support_new_article" = "Новая статья";
"support_new_category" = "Новая категория";
"support_article_acc" = "статью";
"support_category_acc" = "категорию";
"support_language" = "Язык";
"support_category_not_specified" = "Вы не указали категорию";
"support_cant_change_lang_no_cats" = "В этой локализации еще не создано ни одной категории, в которую можно было бы переместить статью";
"support_copy" = "дублировать";
"support_delete_articles" = "Удалить статьи";
"support_copy_articles" = "Копировать со статьями";
/* Invite */