Dmitriy | Инициализация.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
vendor
|
||||
composer.lock
|
||||
62
assets/css/options-page.css
Normal file
62
assets/css/options-page.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.price-updates-options {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.price-updates-options input,
|
||||
.price-updates-options label,
|
||||
.price-updates-options button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.price-updates-options__input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.price-updates-options__input-wrapper input[type="text"] {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.price-updates-options__submit-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.price-updates-options__submit {
|
||||
width: 200px;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background: radial-gradient(278.91% 196.13% at 128.36% -48.29%, #ee6868 0%, #569ef0 57.69%);
|
||||
cursor: pointer;
|
||||
transition: filter 0.3s ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.price-updates-options__submit:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.price-updates-options__success-message,
|
||||
.price-updates-options__error-message {
|
||||
width: max-content;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.price-updates-options__success-message {
|
||||
color: #fff;
|
||||
background-color: #19a917;
|
||||
}
|
||||
|
||||
.price-updates-options__error-message {
|
||||
color: #fff;
|
||||
background-color: #fa0505;
|
||||
}
|
||||
181
assets/css/price-updates-page.css
Normal file
181
assets/css/price-updates-page.css
Normal file
@@ -0,0 +1,181 @@
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.price-updates-wrapper {
|
||||
margin-right: 20px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.price-updates-wrapper input,
|
||||
.price-updates-wrapper label,
|
||||
.price-updates-wrapper button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.price-updates-variations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.price-updates-contents {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.price-updates-contents form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.price-updates-contents input {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.price-updates-contents input[type="url"] {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.price-updates-contents button {
|
||||
width: 200px;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background: radial-gradient(278.91% 196.13% at 128.36% -48.29%, #ee6868 0%, #569ef0 57.69%);
|
||||
cursor: pointer;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.price-updates-contents button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.price-updates-caption {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.price-updates-count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.price-updates-count-success span {
|
||||
color: #19a917;
|
||||
}
|
||||
|
||||
.price-updates-count-error span {
|
||||
color: #fa0505;
|
||||
}
|
||||
|
||||
.price-updates-response {
|
||||
width: 100%;
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.price-updates-response table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-updates-response thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-block: 12px;
|
||||
background-color: #f0f0f1;
|
||||
}
|
||||
|
||||
.price-updates-response tbody td {
|
||||
text-align: center;
|
||||
padding-block: 12px;
|
||||
}
|
||||
|
||||
.price-updates-error {
|
||||
width: max-content;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background-color: #fa0505;
|
||||
}
|
||||
|
||||
.price-updates-loader-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.price-updates-loader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 6rem;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.price-updates-loader:before,
|
||||
.price-updates-loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
animation: pulsOut 1.8s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 1rem rgba(255, 255, 255, 0.75));
|
||||
}
|
||||
|
||||
.price-updates-loader:before {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
box-shadow: inset 0 0 0 1rem #fff;
|
||||
animation-name: pulsIn;
|
||||
}
|
||||
|
||||
.price-updates-loader:after {
|
||||
width: calc(100% - 2rem);
|
||||
padding-bottom: calc(100% - 2rem);
|
||||
box-shadow: 0 0 0 0 #fff;
|
||||
}
|
||||
|
||||
@keyframes pulsIn {
|
||||
0% {
|
||||
box-shadow: inset 0 0 0 1rem #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50%, 100% {
|
||||
box-shadow: inset 0 0 0 0 #fff;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulsOut {
|
||||
0%, 50% {
|
||||
box-shadow: 0 0 0 0 #fff;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 1rem #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
69
assets/js/options-page.js
Normal file
69
assets/js/options-page.js
Normal file
@@ -0,0 +1,69 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const $form = $(".price-updates-options__form");
|
||||
const $loaderWrapper = $(".price-updates-loader-wrapper");
|
||||
|
||||
const $success = $(".price-updates-options__success-message");
|
||||
const $error = $(".price-updates-options__error-message");
|
||||
|
||||
function hideMessages() {
|
||||
$success.addClass("hidden");
|
||||
$error.addClass("hidden")
|
||||
}
|
||||
|
||||
// [Событие] Сохранение настроек
|
||||
$form.on("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$loaderWrapper.removeClass("hidden");
|
||||
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: priceUpdatesOptionsSettings.ajaxUrl + "/update",
|
||||
data: $(this).serializeArray(),
|
||||
success: function (response) {
|
||||
hideMessages();
|
||||
|
||||
$success.removeClass("hidden");
|
||||
$success.text("Настройки обновлены.");
|
||||
},
|
||||
error: function (error) {
|
||||
console.error(error)
|
||||
|
||||
hideMessages();
|
||||
|
||||
$error.removeClass("hidden");
|
||||
$error.text("Ошибка при обновлении настроек.");
|
||||
},
|
||||
complete: function () {
|
||||
$loaderWrapper.addClass("hidden");
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$loaderWrapper.removeClass("hidden");
|
||||
|
||||
// [Ajax] Подгружаем настройки
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: priceUpdatesOptionsSettings.ajaxUrl + "/get",
|
||||
success: function (response) {
|
||||
if (!response) return;
|
||||
|
||||
for (const [key, value] of Object.entries(response)) {
|
||||
const $input = $form.find(`input[name="${key}"]`);
|
||||
$input.val(JSON.stringify(value).slice(1, -1));
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
console.error(error);
|
||||
|
||||
hideMessages();
|
||||
|
||||
$error.removeClass("hidden");
|
||||
$error.text("Ошибка при загрузке настроек.");
|
||||
},
|
||||
complete: function () {
|
||||
$loaderWrapper.addClass("hidden");
|
||||
}
|
||||
})
|
||||
});
|
||||
205
assets/js/price-updates-page.js
Normal file
205
assets/js/price-updates-page.js
Normal file
@@ -0,0 +1,205 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const $variantons = $('input[name="price-updates-variant"]');
|
||||
const $contents = $("form[data-price-updates-variant]");
|
||||
|
||||
const $count = $(".price-updates-count");
|
||||
const $countSuccess = $(".price-updates-count-success");
|
||||
const $countError = $(".price-updates-count-error");
|
||||
|
||||
const $tableWrapper = $(".price-updates-response");
|
||||
const $error = $(".price-updates-error");
|
||||
const $loaderWrapper = $(".price-updates-loader-wrapper");
|
||||
|
||||
// [Событие] Смена варианита обновления цен
|
||||
$variantons.on("change", function () {
|
||||
const $varianton = $(this);
|
||||
|
||||
$tableWrapper.find("tbody").html("");
|
||||
$tableWrapper.addClass("hidden");
|
||||
|
||||
updatePriceUpdatesCountSuccess(0);
|
||||
updatePriceUpdatesCountError(0);
|
||||
|
||||
$count.addClass("hidden");
|
||||
$countSuccess.addClass("hidden");
|
||||
$countError.addClass("hidden");
|
||||
|
||||
$contents.each(function() {
|
||||
const $content = $(this);
|
||||
|
||||
if ($varianton.val() != $content.data("price-updates-variant")) {
|
||||
$content.addClass("hidden");
|
||||
} else {
|
||||
$content.removeClass("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Инициализация вариантов обновления цен
|
||||
$variantons.each(function() {
|
||||
const $varianton = $(this);
|
||||
|
||||
$contents.each(function() {
|
||||
const $content = $(this);
|
||||
|
||||
if (!$varianton.is(":checked") && $varianton.val() == $content.data("price-updates-variant")) {
|
||||
$content.addClass("hidden");
|
||||
} else {
|
||||
$content.removeClass("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getCount(response) {
|
||||
if (!response?.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let success = 0;
|
||||
let error = 0;
|
||||
|
||||
for (const item of response) {
|
||||
if (!item?.isError) {
|
||||
success += 1;
|
||||
} else {
|
||||
error += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
|
||||
function updatePriceUpdatesCountSuccess(count) {
|
||||
if (typeof count !== "number") return;
|
||||
|
||||
$count.removeClass("hidden");
|
||||
|
||||
$countSuccess.removeClass("hidden");
|
||||
$countSuccess.html(`Измененно элементов: <span>${count}</span>`);
|
||||
}
|
||||
|
||||
function updatePriceUpdatesCountError(count) {
|
||||
if (typeof count !== "number") return;
|
||||
|
||||
$count.removeClass("hidden");
|
||||
|
||||
$countError.removeClass("hidden");
|
||||
$countError.html(`Неудалось изменить элементов: <span>${count}</span>`);
|
||||
}
|
||||
|
||||
function getTableRow(data) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const tempData = { ...data };
|
||||
|
||||
delete tempData.url;
|
||||
delete tempData.currency;
|
||||
|
||||
for (const [key, value] of Object.entries(tempData)) {
|
||||
const ceil = document.createElement("td");
|
||||
|
||||
if (key === "sku") {
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = data.url;
|
||||
link.textContent = value;
|
||||
|
||||
ceil.appendChild(link);
|
||||
} else if (key === "regular" || key === "sale") {
|
||||
ceil.textContent = value["old"] != value["new"] ? `${value["old"]} ${data.currency} -> ${value["new"]} ${data.currency}` : `${value["new"]} ${data.currency}`;
|
||||
}
|
||||
|
||||
row.appendChild(ceil);
|
||||
}
|
||||
|
||||
return $(row);
|
||||
}
|
||||
|
||||
function getTableRowError(data) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const tempData = {
|
||||
sku: data?.sku,
|
||||
message: data?.message
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(tempData)) {
|
||||
const ceil = document.createElement("td");
|
||||
|
||||
ceil.textContent = value;
|
||||
|
||||
if (key === "message") {
|
||||
ceil.colSpan = 2;
|
||||
}
|
||||
|
||||
row.appendChild(ceil);
|
||||
}
|
||||
|
||||
return $(row);
|
||||
}
|
||||
|
||||
// [Событие] Отправка формы
|
||||
$contents.on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$loaderWrapper.removeClass("hidden");
|
||||
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: priceUpdatesSettings.ajaxUrl + "/update",
|
||||
processData: false,
|
||||
contentType: false,
|
||||
data: new FormData(this),
|
||||
success: function (response) {
|
||||
$error.addClass("hidden");
|
||||
|
||||
$tableWrapper.find("tbody").html("");
|
||||
|
||||
if (!response?.length) {
|
||||
$tableWrapper.addClass("hidden");
|
||||
|
||||
updatePriceUpdatesCountSuccess(0);
|
||||
updatePriceUpdatesCountError(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const count = getCount(response);
|
||||
|
||||
updatePriceUpdatesCountSuccess(count.success);
|
||||
updatePriceUpdatesCountError(count.error);
|
||||
|
||||
$tableWrapper.removeClass("hidden");
|
||||
|
||||
for (const item of response) {
|
||||
let $row = null;
|
||||
|
||||
if (!item?.isError) {
|
||||
$row = getTableRow(item);
|
||||
} else {
|
||||
$row = getTableRowError(item);
|
||||
}
|
||||
|
||||
$tableWrapper.find("tbody").append($row);
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
$tableWrapper.find("tbody").html("");
|
||||
$tableWrapper.addClass("hidden");
|
||||
|
||||
updatePriceUpdatesCountSuccess(0);
|
||||
updatePriceUpdatesCountError(0);
|
||||
|
||||
$count.addClass("hidden");
|
||||
$countSuccess.addClass("hidden");
|
||||
$countError.addClass("hidden");
|
||||
|
||||
$error.removeClass("hidden");
|
||||
$error.text(error?.responseJSON?.message || "Неизвестная ошибка");
|
||||
},
|
||||
complete: function () {
|
||||
$loaderWrapper.addClass("hidden");
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
24
composer.json
Normal file
24
composer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "cosmopet/price-updates",
|
||||
"description": "Update price for products",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cosmopet\\PriceUpdates\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dmitriy",
|
||||
"email": "Quali123@list.ru"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.0"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"shuchkin/simplexlsx": "^1.1",
|
||||
"google/apiclient": "^2.18"
|
||||
}
|
||||
}
|
||||
13
config/google/credentials.json
Normal file
13
config/google/credentials.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "",
|
||||
"private_key_id": "",
|
||||
"private_key": "",
|
||||
"client_email": "",
|
||||
"client_id": "",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gs-648%40weighty-forest-484418-g1.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
38
price-updates.php
Normal file
38
price-updates.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Price Updates
|
||||
* Description: Update price for products
|
||||
* Version: 1.0.0
|
||||
* Requires PHP: 8.0
|
||||
* Author: Good Projects
|
||||
*
|
||||
* @package price-updates
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
define("PRICE_UPDATES_VERSION", "1.0.0");
|
||||
define("PRICE_UPDATES_PLUGIN_DIR", __DIR__);
|
||||
define("PRICE_UPDATES_PLUGIN_URL", plugin_dir_url(__FILE__));
|
||||
|
||||
use Cosmopet\PriceUpdates\RestApi;
|
||||
use Cosmopet\PriceUpdates\Admin\Menu;
|
||||
|
||||
use Cosmopet\PriceUpdates\Parser\GoogleTableParser;
|
||||
|
||||
class PriceUpdates {
|
||||
public function __construct() {
|
||||
// Инициализация меню и страниц в админ панели
|
||||
Menu::init();
|
||||
|
||||
// Регистрация rest api контроллеров
|
||||
RestApi::register();
|
||||
|
||||
// Инициализируем парсер гугл-таблиц (клиента и сервиса для работы с ней)
|
||||
GoogleTableParser::init();
|
||||
}
|
||||
}
|
||||
|
||||
new PriceUpdates();
|
||||
17
src/Admin/Menu.php
Normal file
17
src/Admin/Menu.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Admin;
|
||||
|
||||
class Menu {
|
||||
public static function init(): void {
|
||||
add_action("admin_menu", function () {
|
||||
PriceUpdatesMenu::init();
|
||||
OptionsSubMenu::init(PriceUpdatesMenu::getSlug());
|
||||
});
|
||||
|
||||
add_action("admin_enqueue_scripts", function ($hook) {
|
||||
PriceUpdatesMenu::enqueue($hook);
|
||||
OptionsSubMenu::enqueue($hook);
|
||||
});
|
||||
}
|
||||
}
|
||||
45
src/Admin/OptionsSubMenu.php
Normal file
45
src/Admin/OptionsSubMenu.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Admin;
|
||||
|
||||
use Cosmopet\PriceUpdates\RestApi;
|
||||
use Cosmopet\PriceUpdates\Controllers\OptionsController;
|
||||
|
||||
class OptionsSubMenu {
|
||||
private static string $slug = "price-updates-options";
|
||||
|
||||
public static function init(string $parentSlug): void {
|
||||
add_submenu_page(
|
||||
$parentSlug,
|
||||
"Настройки",
|
||||
"Настройки",
|
||||
"manage_options",
|
||||
self::$slug,
|
||||
fn () => require_once(PRICE_UPDATES_PLUGIN_DIR . "/src/Views/OptionsPage.php"),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public static function enqueue($hook) {
|
||||
if (!str_contains($hook, self::$slug)) return;
|
||||
|
||||
wp_enqueue_style(
|
||||
self::$slug . "-style",
|
||||
PRICE_UPDATES_PLUGIN_URL . "assets/css/options-page.css",
|
||||
[],
|
||||
filemtime(PRICE_UPDATES_PLUGIN_DIR . '/assets/css/options-page.css')
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
self::$slug . "-script",
|
||||
PRICE_UPDATES_PLUGIN_URL . "assets/js/options-page.js",
|
||||
["jquery"],
|
||||
filemtime(PRICE_UPDATES_PLUGIN_DIR . '/assets/js/options-page.js'),
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(self::$slug . "-script", "priceUpdatesOptionsSettings", [
|
||||
"ajaxUrl" => RestApi::getAjaxUrl() . OptionsController::getNamespace(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
48
src/Admin/PriceUpdatesMenu.php
Normal file
48
src/Admin/PriceUpdatesMenu.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Admin;
|
||||
|
||||
use Cosmopet\PriceUpdates\RestApi;
|
||||
|
||||
class PriceUpdatesMenu {
|
||||
private static string $slug = "price-updates";
|
||||
|
||||
public static function init(): void {
|
||||
add_menu_page(
|
||||
"Обновление цен",
|
||||
"Обновление цен",
|
||||
"manage_options",
|
||||
self::$slug,
|
||||
fn () => require_once(PRICE_UPDATES_PLUGIN_DIR . "/src/Views/PriceUpdatesPage.php"),
|
||||
"dashicons-cloud",
|
||||
76
|
||||
);
|
||||
}
|
||||
|
||||
public static function enqueue($hook) {
|
||||
if (!str_contains($hook, self::$slug)) return;
|
||||
|
||||
wp_enqueue_style(
|
||||
self::$slug . "-style",
|
||||
PRICE_UPDATES_PLUGIN_URL . "assets/css/price-updates-page.css",
|
||||
[],
|
||||
filemtime(PRICE_UPDATES_PLUGIN_DIR . '/assets/css/price-updates-page.css')
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
self::$slug . "-script",
|
||||
PRICE_UPDATES_PLUGIN_URL . "assets/js/price-updates-page.js",
|
||||
["jquery"],
|
||||
filemtime(PRICE_UPDATES_PLUGIN_DIR . '/assets/js/price-updates-page.js'),
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(self::$slug . "-script", "priceUpdatesSettings", [
|
||||
"ajaxUrl" => RestApi::getAjaxUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getSlug(): string {
|
||||
return self::$slug;
|
||||
}
|
||||
}
|
||||
61
src/Classes/ParseProduct.php
Normal file
61
src/Classes/ParseProduct.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Classes;
|
||||
|
||||
class ParseProduct {
|
||||
/**
|
||||
* @var string Артикул товара
|
||||
*/
|
||||
private string $sku;
|
||||
|
||||
/**
|
||||
* @var int Цены товара (Базовая и Аукционная)
|
||||
*/
|
||||
private int $price = 0;
|
||||
|
||||
public function __construct(string $sku, int $price) {
|
||||
$this->setSku($sku);
|
||||
$this->setPrice($price);
|
||||
}
|
||||
|
||||
public static function validateSku($sku): bool {
|
||||
if (gettype($sku) !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validatePrice($price): bool {
|
||||
if (!is_numeric($price)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getSku(): string {
|
||||
return $this->sku;
|
||||
}
|
||||
|
||||
public function setSku(string $sku): ParseProduct {
|
||||
$this->sku = $sku;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrice(): int {
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
public function setPrice(int $price): ParseProduct {
|
||||
$this->price = $price;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
"sku" => $this->getSku(),
|
||||
"price" => $this->getPrice(),
|
||||
];
|
||||
}
|
||||
}
|
||||
45
src/Classes/ParseProductError.php
Normal file
45
src/Classes/ParseProductError.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Classes;
|
||||
|
||||
class ParseProductError {
|
||||
/**
|
||||
* @var string Артикул товара
|
||||
*/
|
||||
private string $sku;
|
||||
|
||||
/**
|
||||
* @var string Сообщение об ошибке
|
||||
*/
|
||||
private string $message;
|
||||
|
||||
public function __construct(string $sku, string $message) {
|
||||
$this->setSku($sku);
|
||||
$this->setMessage($message);
|
||||
}
|
||||
|
||||
public function getSku(): string {
|
||||
return $this->sku;
|
||||
}
|
||||
|
||||
public function setSku(string $sku): ParseProductError {
|
||||
$this->sku = $sku;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage(): string {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMesage(string $message): ParseProductError {
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
"sku" => $this->getSku(),
|
||||
"message" => $this->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
21
src/Controllers/AbstractController.php
Normal file
21
src/Controllers/AbstractController.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Controllers;
|
||||
|
||||
use Cosmopet\PriceUpdates\RestApi;
|
||||
use Cosmopet\PriceUpdates\Controllers\Interface\RegisterControllerInterface;
|
||||
|
||||
class AbstractController implements RegisterControllerInterface {
|
||||
protected static string $namespace = "";
|
||||
protected static array $methods = [];
|
||||
|
||||
public static function register(): void {
|
||||
foreach (static::$methods as $method) {
|
||||
register_rest_route(RestApi::$uri . static::$namespace, $method["uri"], array_diff_key($method, ["uri"]));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getNamespace(): string {
|
||||
return static::$namespace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Controllers\Interface;
|
||||
|
||||
interface RegisterControllerInterface {
|
||||
public static function register(): void;
|
||||
}
|
||||
55
src/Controllers/OptionsController.php
Normal file
55
src/Controllers/OptionsController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Controllers;
|
||||
|
||||
use Cosmopet\PriceUpdates\Services\OptionsService;
|
||||
|
||||
use Cosmopet\PriceUpdates\Parser\GoogleTableParser;
|
||||
|
||||
class OptionsController extends AbstractController {
|
||||
protected static string $namespace = "/options";
|
||||
protected static array $methods = [
|
||||
[
|
||||
"uri" => "/get/",
|
||||
"methods" => "GET",
|
||||
"callback" => ["\Cosmopet\PriceUpdates\Controllers\OptionsController", "get"],
|
||||
],
|
||||
[
|
||||
"uri" => "/update/",
|
||||
"methods" => "POST",
|
||||
"callback" => ["\Cosmopet\PriceUpdates\Controllers\OptionsController", "update"],
|
||||
],
|
||||
];
|
||||
|
||||
public static function get() {
|
||||
$response = OptionsService::get();
|
||||
|
||||
if (isset($data["error"])) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => $data["error"],
|
||||
], 404);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
wp_send_json($response, 200, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
public static function update() {
|
||||
$result = OptionsService::update($_POST);
|
||||
|
||||
if (OptionsService::update($_POST) === false) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Ошибка сохранения настроек.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
GoogleTableParser::init();
|
||||
|
||||
wp_send_json($result, 200);
|
||||
}
|
||||
}
|
||||
118
src/Controllers/PriceUpdatesController.php
Normal file
118
src/Controllers/PriceUpdatesController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Controllers;
|
||||
|
||||
use Cosmopet\PriceUpdates\Services\WCPriceUpdate;
|
||||
|
||||
use Cosmopet\PriceUpdates\Parser\ExcelParser;
|
||||
use Cosmopet\PriceUpdates\Parser\GoogleTableParser;
|
||||
|
||||
class PriceUpdatesController extends AbstractController {
|
||||
private static array $allowedExtensions = ["xls", "xlsx"];
|
||||
|
||||
protected static array $methods = [
|
||||
[
|
||||
"uri" => "/update/",
|
||||
"methods" => "POST",
|
||||
"callback" => ["\Cosmopet\PriceUpdates\Controllers\PriceUpdatesController", "update"],
|
||||
],
|
||||
];
|
||||
|
||||
public static function update() {
|
||||
if (!isset($_POST["type"])){
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Тип обнвления отсутствует.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
switch ($_POST["type"]) {
|
||||
case "excel":
|
||||
if (!isset($_FILES["file"])) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Файл отсутствует.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$fileExtension = strtolower(pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($fileExtension, self::$allowedExtensions)) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Разрешены только файлы с типом XLX или XLXS.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$data = ExcelParser::parse($_FILES["file"]);
|
||||
|
||||
if (isset($data["error"])) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => $data["error"],
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
wp_send_json(WCPriceUpdate::update($data), 200, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
break;
|
||||
|
||||
case "google-table":
|
||||
if (!isset($_POST["url"])){
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Нобходимо указать ссылку.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$url = $_POST["url"];
|
||||
|
||||
if (gettype($url) !== "string"){
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Неверный тип ссылки.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
if (!GoogleTableParser::isValidGoogleSheetsUrl($url)){
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => "Ссылка должна вести на гугл-таблицу.",
|
||||
], 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$data = GoogleTableParser::parse($url);
|
||||
|
||||
if (isset($data["error"])) {
|
||||
wp_send_json([
|
||||
"error" => true,
|
||||
"message" => $data["error"],
|
||||
], !empty($data["code"]) ? $data["code"] : 400);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
wp_send_json(WCPriceUpdate::update($data), 200, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function can() {
|
||||
return current_user_can("manage_options");
|
||||
}
|
||||
}
|
||||
16
src/Parser/AbstractParser.php
Normal file
16
src/Parser/AbstractParser.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Parser;
|
||||
|
||||
use Cosmopet\PriceUpdates\Parser\Interface\ParserInterface;
|
||||
|
||||
abstract class AbstractParser implements ParserInterface {
|
||||
/**
|
||||
* @param $data Файл
|
||||
*
|
||||
* @return \Cosmopet\PriceUpdates\Classes\ParseProduct[]
|
||||
*/
|
||||
public static function parse($data): array {
|
||||
return self::$data;
|
||||
}
|
||||
}
|
||||
55
src/Parser/ExcelParser.php
Normal file
55
src/Parser/ExcelParser.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Parser;
|
||||
|
||||
use Shuchkin\SimpleXLSX;
|
||||
|
||||
use Cosmopet\PriceUpdates\Classes\ParseProduct;
|
||||
|
||||
class ExcelParser extends AbstractParser {
|
||||
public static function parse($data): array {
|
||||
$file = wp_handle_upload($data, [ "test_form" => false ]);
|
||||
|
||||
if (!isset($file["file"])) {
|
||||
return [
|
||||
"error" => "Ошибка обработки файла.",
|
||||
];
|
||||
}
|
||||
|
||||
$parse = SimpleXLSX::parse($file["file"]);
|
||||
|
||||
unlink($file["file"]);
|
||||
|
||||
if ($parse === false) {
|
||||
return [
|
||||
"error" => SimpleXLSX::parseError(),
|
||||
];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach($parse->rows() as $index => $row) {
|
||||
if ($index === 0) continue;
|
||||
|
||||
$sku = $row[0];
|
||||
$price = intval($row[1]);
|
||||
|
||||
if (!ParseProduct::validateSku($sku)) {
|
||||
$ceilNumber = $index + 1;
|
||||
|
||||
$result[] = new ParseProductError("Номер строки: $ceilNumber", "Неверный артикул товара.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ParseProduct::validatePrice($price)) {
|
||||
$result[] = new ParseProductError($sku, "Неверный формат цены.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = new ParseProduct($sku, $price);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
103
src/Parser/GoogleTableParser.php
Normal file
103
src/Parser/GoogleTableParser.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Parser;
|
||||
|
||||
use Cosmopet\PriceUpdates\Classes\ParseProduct;
|
||||
use Cosmopet\PriceUpdates\Classes\ParseProductError;
|
||||
|
||||
use Cosmopet\PriceUpdates\Services\OptionsService;
|
||||
|
||||
use Cosmopet\PriceUpdates\Utils\GoogleAuthConfigValidator;
|
||||
|
||||
class GoogleTableParser extends AbstractParser {
|
||||
private static \Google_Service_Sheets $service;
|
||||
|
||||
public static function init() {
|
||||
$client = new \Google_Client();
|
||||
|
||||
$client->setApplicationName('Google Sheets API');
|
||||
$client->setScopes([\Google_Service_Sheets::SPREADSHEETS]);
|
||||
$client->setAccessType('offline');
|
||||
|
||||
$file = file_get_contents(OptionsService::$filePath);
|
||||
$data = json_decode($file, true);
|
||||
|
||||
if (GoogleAuthConfigValidator::validate($data)) {
|
||||
$client->setAuthConfig(OptionsService::$filePath);
|
||||
}
|
||||
|
||||
self::$service = new \Google_Service_Sheets($client);
|
||||
}
|
||||
|
||||
public static function parse($data): array {
|
||||
preg_match('/\/d\/([a-zA-Z0-9-_]+)/', $data, $matches);
|
||||
|
||||
$spreadsheetId = $matches[1];
|
||||
|
||||
if (empty($spreadsheetId)) {
|
||||
return [
|
||||
"error" => "ID Гугл таблицы отсутствует.",
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = self::$service->spreadsheets_values->get($spreadsheetId, "A:B")?->values;
|
||||
|
||||
if (count($rows) === 0) return [];
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach($rows as $index => $row) {
|
||||
if ($index === 0) continue;
|
||||
|
||||
$sku = $row[0];
|
||||
$price = intval($row[1]);
|
||||
|
||||
if (!ParseProduct::validateSku($sku)) {
|
||||
$ceilNumber = $index + 1;
|
||||
|
||||
$result[] = new ParseProductError("Номер строки: $ceilNumber", "Неверный артикул товара.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ParseProduct::validatePrice($price)) {
|
||||
$result[] = new ParseProductError($sku, "Неверный формат цены.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = new ParseProduct($sku, $price);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$error = json_decode($e->getMessage());
|
||||
|
||||
return [
|
||||
"code" => !empty($error?->error?->code) ? $error?->error?->code : 400,
|
||||
"error" => !empty($error?->error?->message) ? $error?->error?->message : "Ошибка при обработке гугл-таблицы.",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public static function isValidGoogleSheetsUrl(string $url): bool {
|
||||
$patterns = [
|
||||
// Стандартные ссылки на таблицы
|
||||
'~^https?://docs\.google\.com/spreadsheets/(?:d|u/\d+)/([a-zA-Z0-9-_]+)~i',
|
||||
|
||||
// Ссылки через Google Drive
|
||||
'~^https?://drive\.google\.com/(?:open\?.*id=|file/d/)([a-zA-Z0-9-_]+).*[?&]usp=sheets~i',
|
||||
|
||||
// Ссылки на публичные таблицы
|
||||
'~^https?://docs\.google\.com/spreadsheets/d/e/[a-zA-Z0-9-_]+/pub~i',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
7
src/Parser/Interface/ParserInterface.php
Normal file
7
src/Parser/Interface/ParserInterface.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Parser\Interface;
|
||||
|
||||
interface ParserInterface {
|
||||
public static function parse($data): array;
|
||||
}
|
||||
26
src/RestApi.php
Normal file
26
src/RestApi.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates;
|
||||
|
||||
class RestApi {
|
||||
public static string $uri = "price-updates/api/v1";
|
||||
|
||||
private static array $controllers = [
|
||||
"\Cosmopet\PriceUpdates\Controllers\PriceUpdatesController",
|
||||
"\Cosmopet\PriceUpdates\Controllers\OptionsController",
|
||||
];
|
||||
|
||||
public static function register(): void {
|
||||
add_action("rest_api_init", function() {
|
||||
foreach(self::$controllers as $controller) {
|
||||
if (method_exists($controller, "register")) {
|
||||
$controller::register();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function getAjaxUrl(): string {
|
||||
return get_rest_url(null, self::$uri);
|
||||
}
|
||||
}
|
||||
27
src/Services/OptionsService.php
Normal file
27
src/Services/OptionsService.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Services;
|
||||
|
||||
use Cosmopet\PriceUpdates\Utils\GoogleAuthConfigValidator;
|
||||
|
||||
class OptionsService {
|
||||
public static string $filePath = PRICE_UPDATES_PLUGIN_DIR . "/config/google/credentials.json";
|
||||
|
||||
public static function get() {
|
||||
$file = file_get_contents(self::$filePath);
|
||||
|
||||
if ($file === false) {
|
||||
return [
|
||||
"error" => "Файл не найден"
|
||||
];
|
||||
}
|
||||
|
||||
return json_decode($file, true);
|
||||
}
|
||||
|
||||
public static function update($data) {
|
||||
$data = array_map(fn ($val) => stripcslashes($val), $data);
|
||||
$data = stripcslashes(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
return file_put_contents(self::$filePath, $data);
|
||||
}
|
||||
}
|
||||
76
src/Services/WCPriceUpdate.php
Normal file
76
src/Services/WCPriceUpdate.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Services;
|
||||
|
||||
use Cosmopet\PriceUpdates\Classes\ParseProductError;
|
||||
|
||||
class WCPriceUpdate {
|
||||
public static function update(array $data): array {
|
||||
if (count($data) === 0) return [];
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach($data as $item) {
|
||||
if ($item instanceof ParseProductError) {
|
||||
$result[] = array_merge($item->toArray(), [ "isError" => true ]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$products = wc_get_products([ "sku" => $item->getSku() ]);
|
||||
|
||||
if (count($products) === 0) {
|
||||
$result[] = array_merge($item->toArray(), [ "message" => "Товар не найден, проверьте артикул.", "isError" => true ]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = $products[0];
|
||||
|
||||
$price = [
|
||||
"regular" => (int) $product->get_regular_price(),
|
||||
"sale" => (int) $product->get_sale_price(),
|
||||
"new" => $item->getPrice(),
|
||||
];
|
||||
|
||||
// Если цена товара равна базовой и акционная цена отсутствует, его нет смысла обновлять, т.к. данные везде совпадают
|
||||
if ($price["regular"] === $price["new"] && $price["sale"] === 0) {
|
||||
$result[] = array_merge($item->toArray(), $price, [ "message" => "Товар не обновлён. Цены совпадают.", "isError" => true ]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Если цена товара меньше базовой, НО равна акционной, его нет смысла обновлять, ибо ничего не обновиться
|
||||
if ($price["sale"] === $price["new"]) {
|
||||
$result[] = array_merge($item->toArray(), $price, [ "message" => "Товар не обновлён. Акционные цены совпадают.", "isError" => true ]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($price["regular"] <= $price["new"]) {
|
||||
$product->set_regular_price($price["new"]);
|
||||
|
||||
if ($price["sale"] > 0) {
|
||||
$product->set_sale_price(0);
|
||||
}
|
||||
} else {
|
||||
$product->set_sale_price($price["new"]);
|
||||
}
|
||||
|
||||
|
||||
$product->save();
|
||||
|
||||
$result[] = [
|
||||
"sku" => $item->getSku(),
|
||||
"url" => $product->get_permalink(),
|
||||
"regular" => [
|
||||
"new" => (int) $product->get_regular_price(),
|
||||
"old" => $price["regular"],
|
||||
],
|
||||
"sale" => [
|
||||
"new" => (int) $product->get_sale_price(),
|
||||
"old" => $price["sale"]
|
||||
],
|
||||
"currency" => get_woocommerce_currency(),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
31
src/Utils/GoogleAuthConfigValidator.php
Normal file
31
src/Utils/GoogleAuthConfigValidator.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Cosmopet\PriceUpdates\Utils;
|
||||
|
||||
class GoogleAuthConfigValidator {
|
||||
private static array $fields = [
|
||||
"type",
|
||||
"project_id",
|
||||
"private_key_id",
|
||||
"private_key",
|
||||
"client_email",
|
||||
"client_id",
|
||||
"auth_uri",
|
||||
"token_uri",
|
||||
"auth_provider_x509_cert_url",
|
||||
"client_x509_cert_url",
|
||||
"universe_domain"
|
||||
];
|
||||
|
||||
public static function validate($data): bool {
|
||||
if (gettype($data) !== "array" || count($data) !== count(self::$fields)) return false;
|
||||
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (!in_array($key, self::$fields)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
57
src/Views/OptionsPage.php
Normal file
57
src/Views/OptionsPage.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<div class="price-updates-options">
|
||||
<h1>Настройки</h1>
|
||||
<form class="price-updates-options__form">
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="type">type</label>
|
||||
<input type="text" id="type" name="type" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="project_id">project_id</label>
|
||||
<input type="text" id="project_id" name="project_id" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="private_key_id">private_key_id</label>
|
||||
<input type="text" id="private_key_id" name="private_key_id" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="private_key">private_key</label>
|
||||
<input type="text" id="private_key" name="private_key" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="client_email">client_email</label>
|
||||
<input type="text" id="client_email" name="client_email" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="client_id">client_id</label>
|
||||
<input type="text" id="client_id" name="client_id" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="auth_uri">auth_uri</label>
|
||||
<input type="text" id="auth_uri" name="auth_uri" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="token_uri">token_uri</label>
|
||||
<input type="text" id="token_uri" name="token_uri" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="auth_provider_x509_cert_url">auth_provider_x509_cert_url</label>
|
||||
<input type="text" id="auth_provider_x509_cert_url" name="auth_provider_x509_cert_url" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="client_x509_cert_url">client_x509_cert_url</label>
|
||||
<input type="text" id="client_x509_cert_url" name="client_x509_cert_url" />
|
||||
</div>
|
||||
<div class="price-updates-options__input-wrapper">
|
||||
<label for="universe_domain">universe_domain</label>
|
||||
<input type="text" id="universe_domain" name="universe_domain" />
|
||||
</div>
|
||||
<div class="price-updates-options__submit-wrapper">
|
||||
<button type="submit" class="price-updates-options__submit">Обновить</button>
|
||||
<div class="price-updates-options__success-message hidden"></div>
|
||||
<div class="price-updates-options__error-message hidden"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="price-updates-loader-wrapper hidden">
|
||||
<div class="price-updates-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
55
src/Views/PriceUpdatesPage.php
Normal file
55
src/Views/PriceUpdatesPage.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<div class="price-updates-wrapper">
|
||||
<h1>Обновление цен у товаров</h1>
|
||||
|
||||
<div class="price-updates-variations">
|
||||
<div>
|
||||
<input type="radio" id="excel" name="price-updates-variant" value="excel" checked />
|
||||
<label for="excel">Обновить через файл Excel</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="google-table" name="price-updates-variant" value="google-table" />
|
||||
<label for="google-table">Обновить через google-таблицу</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-updates-contents">
|
||||
<form data-price-updates-variant="excel">
|
||||
<input type="file" name="file" require />
|
||||
<button type="submit">Обновить</button>
|
||||
<input type="hidden" name="type" value="excel" accept=".xlsx,.xls" />
|
||||
</form>
|
||||
<form data-price-updates-variant="google-table" class="hidden">
|
||||
<input type="url" name="url" placeholder="Ссылка на гугл таблицу" require />
|
||||
<button type="submit">Обновить</button>
|
||||
<input type="hidden" name="type" value="google-table" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<span class="price-updates-caption">
|
||||
Обновляются только те товары, у которых <b>присутствует артикул и цена не совпадает с базовой или акционной</b>.
|
||||
</span>
|
||||
|
||||
<div class="price-updates-count hidden">
|
||||
<span class="price-updates-count-success"></span>
|
||||
<span class="price-updates-count-error"></span>
|
||||
</div>
|
||||
|
||||
<div class="price-updates-response hidden">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Артикул</th>
|
||||
<th>Базовая цена</th>
|
||||
<th>Акционная цена</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="price-updates-error hidden"></div>
|
||||
|
||||
<div class="price-updates-loader-wrapper hidden">
|
||||
<div class="price-updates-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user