diff --git a/Web/Models/Entities/FAQArticle.php b/Web/Models/Entities/FAQArticle.php new file mode 100644 index 00000000..a7d6f89e --- /dev/null +++ b/Web/Models/Entities/FAQArticle.php @@ -0,0 +1,44 @@ +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; + } +} diff --git a/Web/Models/Entities/FAQCategory.php b/Web/Models/Entities/FAQCategory.php new file mode 100644 index 00000000..11789d98 --- /dev/null +++ b/Web/Models/Entities/FAQCategory.php @@ -0,0 +1,50 @@ +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; + } +} diff --git a/Web/Models/Repositories/FAQArticles.php b/Web/Models/Repositories/FAQArticles.php new file mode 100644 index 00000000..a7070f35 --- /dev/null +++ b/Web/Models/Repositories/FAQArticles.php @@ -0,0 +1,27 @@ +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)); + } +} diff --git a/Web/Models/Repositories/FAQCategories.php b/Web/Models/Repositories/FAQCategories.php new file mode 100644 index 00000000..2d74bfde --- /dev/null +++ b/Web/Models/Repositories/FAQCategories.php @@ -0,0 +1,37 @@ +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); + } + } +} diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index c4d729ea..3d5259b4 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -1,7 +1,7 @@ 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"); + } } diff --git a/Web/Presenters/templates/Support/FAQArticle.xml b/Web/Presenters/templates/Support/FAQArticle.xml new file mode 100644 index 00000000..14d64588 --- /dev/null +++ b/Web/Presenters/templates/Support/FAQArticle.xml @@ -0,0 +1,73 @@ +{extends "../@layout.xml"} + +{block title}{$title}{/block} + +{block header} + {_menu_help} + > {$category->getTitle()} + > {$article->getTitle()} +
{_support_article} ({_app_edit})
+
> {_support_edit_mode}
+{/block} + +{block content} + +
+

{$article->getTitle()}

+ {$text|noescape} +
+
+
+
+

+

+ {_support_unlogged_can_see} + {_support_users_can_see} + + + + + +
+ {_support_category}: + + +
+ + + + + +
+ {_support_language}: + + +
+ + +

+
+
+


+
+ + +
+

+{/block} diff --git a/Web/Presenters/templates/Support/FAQCategory.xml b/Web/Presenters/templates/Support/FAQCategory.xml new file mode 100644 index 00000000..0574b28b --- /dev/null +++ b/Web/Presenters/templates/Support/FAQCategory.xml @@ -0,0 +1,90 @@ +{extends "../@layout.xml"} + +{block title}{$title}{/block} + +{block header} + {_menu_help} +
> {_support_faq_category}
+
> {$category->getTitle()} > {_support_edit_mode}
+{/block} + +{block content} + {var $articles = iterator_to_array($category->getArticles(NULL, $canEditFAQ))} +
+

+
+ {$category->getTitle()} +
+ ({_support_edit}) + ({_support_copy}) + (+{_support_add_article}) +
+

+
+ +
+ +
{_support_empty}
+
+
+
+
+
+

+ {_support_for_agents_only} + {_support_copy_articles} +

+
+
+
+
+
+
+ + + + + +
+ {_support_language}: + + +
+ + + + +

+
+
+


+
+ + + {_support_delete_articles} +
+

+ +{/block} diff --git a/Web/Presenters/templates/Support/FAQNewArticle.xml b/Web/Presenters/templates/Support/FAQNewArticle.xml new file mode 100644 index 00000000..cd97a2a3 --- /dev/null +++ b/Web/Presenters/templates/Support/FAQNewArticle.xml @@ -0,0 +1,57 @@ +{extends "../@layout.xml"} + +{block title}{_support_new_article}{/block} + +{block header}{_menu_help} > {_support_create_article}{/block} + +{block content} + +
+
+ + + + + +
+ {_support_language}: + + +
+

+

+ {_support_unlogged_can_see} + {_support_users_can_see} + + + + + +
+ {_support_category}: + + +
+ {$test} + + +

+
+
+{/block} diff --git a/Web/Presenters/templates/Support/FAQNewCategory.xml b/Web/Presenters/templates/Support/FAQNewCategory.xml new file mode 100644 index 00000000..46ce815d --- /dev/null +++ b/Web/Presenters/templates/Support/FAQNewCategory.xml @@ -0,0 +1,56 @@ +{extends "../@layout.xml"} + +{block title}{_support_new_category}{/block} + +{block header}{_menu_help} > {_support_create_category}{/block} + +{block content} +
+
+ + + + + +
+ {_support_language}: + + +
+

+ {_support_for_agents_only} +

+
+ Иконка +
+
+
+
+
+
+

+ + + + +

+
+
+ +{/block} diff --git a/Web/Presenters/templates/Support/Index.xml b/Web/Presenters/templates/Support/Index.xml index acb52859..0bcea0dc 100644 --- a/Web/Presenters/templates/Support/Index.xml +++ b/Web/Presenters/templates/Support/Index.xml @@ -49,7 +49,57 @@ {/if} {if $isMain} +

{_support_faq}


+
+
+ {_create} + {_support_category_acc} + · + {_support_article_acc} +
+ + + + + +
+ {_support_language}: + + +
+
+
+
+

+
+ {$category->getTitle()} +

+ {var $articles = iterator_to_array($category->getArticles(3, $canEditFAQ))} + +
+ +
{_support_empty}
+
+
+
+

{$section[0]}
{$section[1]|noescape}
diff --git a/Web/routes.yml b/Web/routes.yml index d1a0e7ae..c4e563a4 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -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: diff --git a/Web/static/img/faq_icons.png b/Web/static/img/faq_icons.png new file mode 100644 index 00000000..888942c7 Binary files /dev/null and b/Web/static/img/faq_icons.png differ diff --git a/Web/static/img/sad.png b/Web/static/img/sad.png new file mode 100644 index 00000000..715c4335 Binary files /dev/null and b/Web/static/img/sad.png differ diff --git a/install/sqls/00038-faq.sql b/install/sqls/00038-faq.sql new file mode 100644 index 00000000..a77e7d82 --- /dev/null +++ b/install/sqls/00038-faq.sql @@ -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; diff --git a/locales/en.strings b/locales/en.strings index 4aaa4483..685cf861 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -913,6 +913,32 @@ "banned_in_support_1" = "Sorry, $1, but now you can't create tickets."; "banned_in_support_2" = "And the reason for this is simple: $1. 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"; diff --git a/locales/ru.strings b/locales/ru.strings index 6faa5e2e..3a714f43 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -841,6 +841,31 @@ "ticket_changed_comment" = "Изменения вступят силу через несколько секунд."; "banned_in_support_1" = "Извините, $1, но теперь вам нельзя создавать обращения."; "banned_in_support_2" = "А причина этому проста: $1. К сожалению, на этот раз нам пришлось отобрать у вас эту возможность навсегда."; +"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 */