forked from Business/AccountHub
Remove home screen widgets, build new i18n system, close #4
parent
3d309ac68b
commit
deca0d330d
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"manage account security" => "Manage account security",
|
||||
"manage security description" => "Review security features or change your password."
|
||||
]
|
||||
]);
|
||||
$APPS["account_security"]["i18n"] = TRUE;
|
||||
$APPS["account_security"]["title"] = "account security";
|
||||
$APPS["account_security"]["icon"] = "lock";
|
||||
$APPS["account_security"]["type"] = "brown";
|
||||
$content = "<p>"
|
||||
. lang("manage security description", false)
|
||||
. '</p> '
|
||||
. '<a href="home.php?page=security" class="btn btn-primary btn-block">'
|
||||
. lang("manage account security", false)
|
||||
. '</a>';
|
||||
$APPS["account_security"]["content"] = $content;
|
||||
?>
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"inventory" => "Inventory",
|
||||
"open inventory system" => "Open the inventory system"
|
||||
]
|
||||
]);
|
||||
$APPS["inventory_link"]["i18n"] = TRUE;
|
||||
$APPS["inventory_link"]["title"] = "inventory";
|
||||
$APPS["inventory_link"]["icon"] = "cubes";
|
||||
$APPS["inventory_link"]["type"] = "teal";
|
||||
$content = "<p class='mobile-app-hide'>" . lang("open inventory system", false) . '</p><a href="' . INVENTORY_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["inventory_link"]["content"] = $content;
|
||||
|
||||
require_once __DIR__ . "/../lib/login.php";
|
||||
if (account_has_permission($_SESSION['username'], "INV_VIEW") !== true) {
|
||||
unset($APPS['inventory_link']);
|
||||
}
|
||||
?>
|
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
require_once __DIR__ . "/../lib/login.php";
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"qwikclock" => "QwikClock",
|
||||
"punch in" => "Punch in",
|
||||
"punch out" => "Punch out",
|
||||
"permission denied" => "You do not have permission to do that."
|
||||
]
|
||||
]);
|
||||
$APPS["qwikclock_inout"]["i18n"] = TRUE;
|
||||
$APPS["qwikclock_inout"]["title"] = "qwikclock";
|
||||
$APPS["qwikclock_inout"]["icon"] = "clock-o";
|
||||
$APPS["qwikclock_inout"]["type"] = "blue";
|
||||
$content = "";
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
if (!is_empty($_GET['qwikclock']) && ($_GET['qwikclock'] === "punchin" || $_GET['qwikclock'] === "punchout")) {
|
||||
try {
|
||||
$client = new GuzzleHttp\Client();
|
||||
|
||||
$response = $client->request('POST', QWIKCLOCK_API, ['form_params' => [
|
||||
'action' => $_GET['qwikclock'],
|
||||
'username' => $_SESSION['username'],
|
||||
'password' => $_SESSION['password']
|
||||
]]);
|
||||
|
||||
$resp = json_decode($response->getBody(), TRUE);
|
||||
if ($resp['status'] == "OK") {
|
||||
$content = "<div class=\"alert alert-success alert-dismissable\"><button type=\"button\" class=\"close\">×</button>" . $resp['msg'] . "</div>";
|
||||
} else {
|
||||
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">×</button>" . $resp['msg'] . "</div>";
|
||||
}
|
||||
} catch (ClientException $e) {
|
||||
if ($e->getResponse()->getStatusCode() == 403) {
|
||||
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">×</button>" . lang("permission denied", false) . "</div>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">×</button>" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
|
||||
}
|
||||
}
|
||||
$lang_punchin = lang("punch in", false);
|
||||
$lang_punchout = lang("punch out", false);
|
||||
$content .= <<<END
|
||||
<a href="home.php?&qwikclock=punchin" class="btn btn-block btn-success btn-lg"><i class="fa fa-play"></i> $lang_punchin</a>
|
||||
<a href="home.php?qwikclock=punchout" class="btn btn-block btn-danger btn-lg"><i class="fa fa-stop"></i> $lang_punchout</a>
|
||||
END;
|
||||
$content .= '<br /><a href="' . QWIKCLOCK_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["qwikclock_inout"]["content"] = $content;
|
||||
|
||||
|
||||
if (account_has_permission($_SESSION['username'], "QWIKCLOCK") !== true) {
|
||||
unset($APPS['qwikclock_inout']);
|
||||
}
|
||||
?>
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
|
||||
// Additional i18n strings
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"sample app" => "Sample Application",
|
||||
]
|
||||
]);
|
||||
// Set to true to automatically parse the app title as a language string.
|
||||
$APPS["sample_app"]["i18n"] = TRUE;
|
||||
// App title.
|
||||
$APPS["sample_app"]["title"] = "sample app";
|
||||
// App icon, from FontAwesome.
|
||||
$APPS["sample_app"]["icon"] = "rocket";
|
||||
// App content.
|
||||
$APPS["sample_app"]["content"] = <<<'CONTENTEND'
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
Item 1
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
Item 2
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
Item 3
|
||||
</div>
|
||||
</div>
|
||||
CONTENTEND;
|
||||
?>
|
@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"messages" => "Messages",
|
||||
"no messages" => "No messages found."
|
||||
]
|
||||
]);
|
||||
$APPS["taskfloor_messages"]["i18n"] = TRUE;
|
||||
$APPS["taskfloor_messages"]["title"] = "messages";
|
||||
$APPS["taskfloor_messages"]["icon"] = "comments";
|
||||
$APPS["taskfloor_messages"]["type"] = "deep-purple";
|
||||
$content = "";
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
try {
|
||||
$client = new GuzzleHttp\Client();
|
||||
|
||||
$response = $client->request('POST', TASKFLOOR_API, ['form_params' => [
|
||||
'action' => "getmsgs",
|
||||
'username' => $_SESSION['username'],
|
||||
'password' => $_SESSION['password'],
|
||||
'max' => 5
|
||||
]]);
|
||||
|
||||
$resp = json_decode($response->getBody(), TRUE);
|
||||
if ($resp['status'] == "OK") {
|
||||
if (count($resp['messages']) > 0) {
|
||||
$content = '<div class="list-group">';
|
||||
foreach ($resp['messages'] as $msg) {
|
||||
$content .= '<div class="list-group-item">';
|
||||
$content .= $msg['text'];
|
||||
$fromuser = $msg['from']['username'];
|
||||
$fromname = $msg['from']['name'];
|
||||
$touser = $msg['to']['username'];
|
||||
$toname = $msg['to']['name'];
|
||||
$content .= <<<END
|
||||
<br />
|
||||
<span class="small">
|
||||
<span data-toggle="tooltip" title="$fromuser">$fromname</span>
|
||||
<i class="fa fa-caret-right"></i>
|
||||
<span data-toggle="tooltip" title="$touser">$toname</span>
|
||||
</span>
|
||||
END;
|
||||
$content .= '</div>';
|
||||
}
|
||||
$content .= "</div>";
|
||||
} else {
|
||||
$content = "<div class=\"alert alert-info\">" . lang("no messages", false) . "</div>";
|
||||
}
|
||||
}
|
||||
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["taskfloor_messages"]["content"] = $content;
|
||||
} catch (ClientException $e) {
|
||||
if ($e->getResponse()->getStatusCode() == 403) {
|
||||
unset($APPS['taskfloor_messages']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$content = "<div class=\"alert alert-danger\">" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
|
||||
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["taskfloor_messages"]["content"] = $content;
|
||||
}
|
||||
?>
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
dieifnotloggedin();
|
||||
addMultiLangStrings(["en_us" => [
|
||||
"tasks" => "Tasks",
|
||||
"no tasks found" => "No tasks found."
|
||||
]
|
||||
]);
|
||||
$APPS["taskfloor_tasks"]["i18n"] = TRUE;
|
||||
$APPS["taskfloor_tasks"]["title"] = "tasks";
|
||||
$APPS["taskfloor_tasks"]["icon"] = "tasks";
|
||||
$APPS["taskfloor_tasks"]["type"] = "blue-grey";
|
||||
$content = "";
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
try {
|
||||
$client = new GuzzleHttp\Client();
|
||||
|
||||
$response = $client->request('POST', TASKFLOOR_API, ['form_params' => [
|
||||
'action' => "gettasks",
|
||||
'username' => $_SESSION['username'],
|
||||
'password' => $_SESSION['password'],
|
||||
'max' => 5
|
||||
]]);
|
||||
|
||||
$resp = json_decode($response->getBody(), TRUE);
|
||||
if ($resp['status'] == "OK") {
|
||||
if (count($resp['tasks']) > 0) {
|
||||
$content = '<div class="list-group">';
|
||||
foreach ($resp['tasks'] as $task) {
|
||||
$content .= '<div class="list-group-item">';
|
||||
$content .= '<i class="fa fa-fw fa-' . $task['icon'] . '"></i> ' . $task['title'] . '';
|
||||
$content .= '</div>';
|
||||
}
|
||||
$content .= "</div>";
|
||||
} else {
|
||||
$content = "<div class=\"alert alert-success\">" . lang("no tasks found", false) . "</div>";
|
||||
}
|
||||
}
|
||||
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["taskfloor_tasks"]["content"] = $content;
|
||||
} catch (ClientException $e) {
|
||||
if ($e->getResponse()->getStatusCode() == 403) {
|
||||
unset($APPS['taskfloor_tasks']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$content = "<div class=\"alert alert-danger\">" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
|
||||
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' <i class="fa fa-external-link-square"></i></a>';
|
||||
$APPS["taskfloor_tasks"]["content"] = $content;
|
||||
}
|
||||
?>
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"setup 2fa": "Setup 2-factor authentication",
|
||||
"2fa removed": "2-factor authentication disabled.",
|
||||
"2fa enabled": "2-factor authentication activated.",
|
||||
"remove 2fa": "Disable 2-factor authentication",
|
||||
"2fa explained": "2-factor authentication adds more security to your account. You can use the Auth Keys (key icon) feature of the Netsyms Business Mobile app, or another TOTP-enabled app (Authy, FreeOTP, etc) on your smartphone. When you have the app installed, you can enable 2-factor authentication by clicking the button below and scanning a QR code with the app. Whenever you sign in in the future, you'll need to input a six-digit code from your phone into the login page when prompted. You can disable 2-factor authentication from this page if you change your mind.",
|
||||
"2fa active": "2-factor authentication is active on your account. To remove 2fa, reset your authentication secret, or change to a new security device, click the button below.",
|
||||
"enable 2fa": "Enable 2-factor authentication",
|
||||
"scan 2fa qrcode": "Scan the QR Code with the authenticator app, or enter the information manually. Then type in the six-digit code the app gives you and press Finish Setup.",
|
||||
"confirm 2fa": "Finish setup",
|
||||
"enter otp code": "Enter 6-digit code",
|
||||
"secret key": "Secret key",
|
||||
"label": "Label",
|
||||
"issuer": "Issuer",
|
||||
"no such code or code expired": "That code is incorrect or expired."
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"user does not exist": "User does not exist."
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
{
|
||||
"sign in": "Sign In",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"continue": "Continue",
|
||||
"authcode": "Authentication code",
|
||||
"2fa prompt": "Enter the six-digit code from your mobile authenticator app.",
|
||||
"2fa incorrect": "Authentication code incorrect.",
|
||||
"login incorrect": "Login incorrect.",
|
||||
"login server unavailable": "Login server unavailable. Try again later or contact technical support.",
|
||||
"account locked": "This account has been disabled. Contact technical support.",
|
||||
"password expired": "You must change your password before continuing.",
|
||||
"account terminated": "Account terminated. Access denied.",
|
||||
"account state error": "Your account state is not stable. Log out, restart your browser, and try again.",
|
||||
"welcome user": "Welcome, {user}!",
|
||||
"sign out": "Sign out",
|
||||
"404 error": "404 Error",
|
||||
"page not found": "Page not found.",
|
||||
"invalid parameters": "Invalid request parameters.",
|
||||
"login server error": "The login server returned an error: {arg}",
|
||||
"login server user data error": "The login server refused to provide account information. Try again or contact technical support.",
|
||||
"captcha error": "There was a problem with the CAPTCHA (robot test). Try again.",
|
||||
"no access permission": "You do not have permission to access this system.",
|
||||
"generic op error": "An unknown error occurred. Try again later.",
|
||||
"home": "Home"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"ldap server error": "The LDAP server returned an error: {arg}",
|
||||
"ldap error": "LDAP error: {error}"
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"password on 500 list": "The given password is ranked number {arg} out of the 500 most common passwords. Try a different one.",
|
||||
"change password": "Change password",
|
||||
"current password incorrect": "The current password is incorrect. Try again.",
|
||||
"new password mismatch": "The new passwords did not match. Try again.",
|
||||
"weak password": "Password does not meet requirements.",
|
||||
"password updated": "Password updated successfully.",
|
||||
"current password": "Current password",
|
||||
"new password": "New password",
|
||||
"confirm password": "New password (again)",
|
||||
"password complexity insufficent": "The new password does not meet the minumum requirements defined by your system administrator.",
|
||||
"old and new passwords match": "Your current and new passwords are the same."
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"pin explanation": "Change or set a login PIN for the Station kiosk Quick Access. PIN codes must be between one and eight digits.",
|
||||
"change pin": "Change PIN",
|
||||
"new pin": "New PIN",
|
||||
"confirm pin": "New PIN (again)",
|
||||
"pin updated": "PIN updated.",
|
||||
"new pin mismatch": "The new PINs don't match each other.",
|
||||
"invalid pin format": "PIN codes must be numeric and between one and eight digits in length."
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sign in again": "Please sign in again to continue.",
|
||||
"login failed try on web": "There is a problem with your account. Visit AccountHub via a web browser for more information.",
|
||||
"mobile login disabled": "Mobile login has been disabled by your system administrator. Contact technical support for more information.",
|
||||
"admin alert email subject": "Alert: User login notification",
|
||||
"admin alert email message": "You (or another administrator) requested to be notified when user \"{username}\" logged in, an event which happened just now.\r\n\r\nUsername: \t{username}\r\nApplication: \t{appname}\r\nDate\/Time: \t{datetime}\r\nIP address: \t{ipaddr}\r\n\r\nThese notifications can be disabled by editing the user in ManagePanel."
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"sync mobile": "Sync Mobile App",
|
||||
"scan sync qrcode": "Scan this code with the mobile app or enter the code manually.",
|
||||
"sync explained": "Access your account and apps on the go. Use a sync code to securely connect your phone or tablet to AccountHub with the Netsyms Business mobile app.",
|
||||
"generate sync": "Create new sync code",
|
||||
"active sync codes": "Active codes",
|
||||
"no active codes": "No active codes.",
|
||||
"done adding sync code": "Done adding code",
|
||||
"manual setup": "Manual Setup:",
|
||||
"sync key": "Sync key:",
|
||||
"url": "URL:"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"account security": "Account security",
|
||||
"security options": "Security options",
|
||||
"account options": "Account options",
|
||||
"sync": "Sync settings",
|
||||
"settings": "Settings",
|
||||
"account": "Account"
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides translated language strings.
|
||||
*/
|
||||
class Strings {
|
||||
|
||||
private $language = "en";
|
||||
private $strings = [];
|
||||
|
||||
function __construct($language = "en") {
|
||||
if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
|
||||
throw new Exception("Invalid language code $language");
|
||||
}
|
||||
|
||||
$this->load("en");
|
||||
|
||||
if (file_exists(__DIR__ . "/../langs/$language/")) {
|
||||
$this->language = $language;
|
||||
$this->load($language);
|
||||
} else {
|
||||
trigger_error("Language $language could not be found.", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all JSON files for the specified language.
|
||||
* @param string $language
|
||||
*/
|
||||
private function load(string $language) {
|
||||
$files = glob(__DIR__ . "/../langs/$language/*.json");
|
||||
foreach ($files as $file) {
|
||||
$strings = json_decode(file_get_contents($file), true);
|
||||
foreach ($strings as $key => $val) {
|
||||
if (array_key_exists($key, $this->strings)) {
|
||||
trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING);
|
||||
}
|
||||
$this->strings[$key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add language strings dynamically.
|
||||
* @param array $strings ["key" => "value", ...]
|
||||
*/
|
||||
function addStrings(array $strings) {
|
||||
foreach ($strings as $key => $val) {
|
||||
$this->strings[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I18N string getter. If the key isn't found, it outputs the key itself.
|
||||
* @param string $key
|
||||
* @param bool $echo True to echo the result, false to return it. Default is true.
|
||||
* @return string
|
||||
*/
|
||||
function get(string $key, bool $echo = true): string {
|
||||
$str = $key;
|
||||
if (array_key_exists($key, $this->strings)) {
|
||||
$str = $this->strings[$key];
|
||||
} else {
|
||||
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
|
||||
}
|
||||
|
||||
if ($echo) {
|
||||
echo $str;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
|
||||
* @param string $key
|
||||
* @param array $replace key-value array of replacements.
|
||||
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
|
||||
* result will be "hello 123".
|
||||
* @param bool $echo True to echo the result, false to return it. Default is true.
|
||||
* @return string
|
||||
*/
|
||||
function build(string $key, array $replace, bool $echo = true): string {
|
||||
$str = $key;
|
||||
if (array_key_exists($key, $this->strings)) {
|
||||
$str = $this->strings[$key];
|
||||
} else {
|
||||
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
|
||||
}
|
||||
|
||||
foreach ($replace as $find => $repl) {
|
||||
$str = str_replace("{" . $find . "}", $repl, $str);
|
||||
}
|
||||
|
||||
if ($echo) {
|
||||
echo $str;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a JSON key:value string for the supplied array of keys.
|
||||
* @param array $keys ["key1", "key2", ...]
|
||||
*/
|
||||
function getJSON(array $keys): string {
|
||||
$strings = [];
|
||||
foreach ($keys as $k) {
|
||||
$strings[$k] = $this->get($k, false);
|
||||
}
|
||||
return json_encode($strings);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue