release 0.267: front layout/basket fixes and product redirect hardening
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
||||
$out .= 'id="' . $this -> params['id'] . '" ';
|
||||
else
|
||||
$out .= 'id="' . $this -> params['name'] . '" ';
|
||||
$out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="1"';
|
||||
$out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="on"';
|
||||
|
||||
if ( $this -> params['checked'] )
|
||||
$out .= 'checked="checked" ';
|
||||
@@ -24,4 +24,4 @@
|
||||
$out .= '</div>';
|
||||
$out .= '</div>';
|
||||
|
||||
echo $out;
|
||||
echo $out;
|
||||
|
||||
339
apilo-bck
339
apilo-bck
@@ -1,339 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const currentUrl = window.location.href;
|
||||
console.log(currentUrl);
|
||||
const patterns = [
|
||||
/^https:\/\/projectpro\.apilo\.com\/order\/order\/news\/?$/,
|
||||
/^https:\/\/projectpro\.apilo\.com\/order\/order\/in-progress\/?$/,
|
||||
/^https:\/\/projectpro\.apilo\.com\/order\/order\/to-send\/?$/,
|
||||
/^https:\/\/projectpro\.apilo\.com\/order\/order\/completed\/?$/,
|
||||
/^https:\/\/projectpro\.apilo\.com\/order\/order\/all\/?$/
|
||||
];
|
||||
if (patterns.some(pattern => pattern.test(currentUrl))) {
|
||||
waitForTableAndSetImage();
|
||||
attachTableReloadListener();
|
||||
}
|
||||
|
||||
function waitForTableAndSetImage() {
|
||||
const intervalId = setInterval(() => {
|
||||
let dataTables_scrollBody = document.getElementsByClassName('dataTables_scrollBody');
|
||||
if (dataTables_scrollBody.length > 0) {
|
||||
let dataTables_tbody = dataTables_scrollBody[0].getElementsByTagName('tbody')[0];
|
||||
let rows = dataTables_tbody.getElementsByTagName('tr');
|
||||
|
||||
if (rows.length > 0) {
|
||||
clearInterval(intervalId);
|
||||
setImageToProduct();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function attachTableReloadListener() {
|
||||
const table = document.querySelector('.dataTables_scrollBody table');
|
||||
if (table) {
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
waitForTableAndSetImage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(table.querySelector('tbody'), { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
function setImageToProduct(img = '') {
|
||||
let dataTables_scrollBody = document.getElementsByClassName('dataTables_scrollBody');
|
||||
if (dataTables_scrollBody.length > 0) {
|
||||
let dataTables_tbody = dataTables_scrollBody[0].getElementsByTagName('tbody')[0];
|
||||
let rows = dataTables_tbody.getElementsByTagName('tr');
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let cells = rows[i].getElementsByTagName('td');
|
||||
|
||||
if (cells.length > 1) {
|
||||
let secondCellText = cells[1].textContent.trim();
|
||||
let domain;
|
||||
|
||||
if (secondCellText.includes('marianek.pl')) {
|
||||
domain = 'marianek.pl';
|
||||
} else if (secondCellText.includes('pomysloweprezenty.pl')) {
|
||||
domain = 'pomysloweprezenty.pl';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cells.length >= 5) {
|
||||
let fifthCell = cells[4];
|
||||
let divsInFifthCell = fifthCell.children;
|
||||
|
||||
for (let i = 0; i < divsInFifthCell.length; i++) {
|
||||
let currentDiv = divsInFifthCell[i];
|
||||
let imgDiv = currentDiv.getElementsByTagName('div')[0];
|
||||
let dataDiv = currentDiv.getElementsByTagName('div')[1];
|
||||
|
||||
if (dataDiv) {
|
||||
let skuText = dataDiv.innerHTML.match(/SKU:\s*([A-Za-z0-9-]+)/);
|
||||
|
||||
if (skuText && skuText[1]) {
|
||||
getProductData(skuText[1], domain).then(data => {
|
||||
if (!data) {
|
||||
console.log('Product not found:', skuText[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Product found:', skuText[1]);
|
||||
imgDiv.innerHTML = '';
|
||||
const imgElement = makeImg(data);
|
||||
imgDiv.appendChild(imgElement);
|
||||
|
||||
imgElement.addEventListener('mouseover', function() {
|
||||
showLargeImage(data, imgElement);
|
||||
});
|
||||
|
||||
imgElement.addEventListener('mouseout', function() {
|
||||
hideLargeImage();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeImg(src = 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png') {
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
img.alt = 'image';
|
||||
img.className = 'img-fluid center-block';
|
||||
img.style.maxWidth = '48px';
|
||||
img.style.maxHeight = '48px';
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
function showLargeImage(src, imgElement) {
|
||||
const largeImg = document.createElement('img');
|
||||
largeImg.src = src;
|
||||
largeImg.style.position = 'absolute';
|
||||
largeImg.style.maxWidth = '400px';
|
||||
largeImg.style.maxHeight = '400px';
|
||||
largeImg.style.border = '1px solid #ccc';
|
||||
largeImg.style.background = '#fff';
|
||||
largeImg.style.zIndex = '1000';
|
||||
largeImg.style.top = imgElement.getBoundingClientRect().top + window.scrollY + 'px';
|
||||
largeImg.style.left = imgElement.getBoundingClientRect().left + imgElement.offsetWidth + 10 + 'px';
|
||||
largeImg.id = 'largeImagePreview';
|
||||
|
||||
document.body.appendChild(largeImg);
|
||||
}
|
||||
|
||||
function hideLargeImage() {
|
||||
const largeImg = document.getElementById('largeImagePreview');
|
||||
if (largeImg) {
|
||||
largeImg.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function getProductData(sku, domain) {
|
||||
let url;
|
||||
if (domain === 'marianek.pl') {
|
||||
url = `https://marianek.pl/api/v1/product.php?sku=${sku}`;
|
||||
} else if (domain === 'pomysloweprezenty.pl') {
|
||||
url = `https://pomysloweprezenty.pl/api/v1/product.php?sku=${sku}`;
|
||||
} else {
|
||||
console.error('Unsupported domain:', domain);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json(); console.log(data);
|
||||
return data.img;
|
||||
} catch (error) {
|
||||
console.error('Error fetching product data: ' + url, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const currentUrl2 = window.location.href;
|
||||
const pattern = /^https:\/\/projectpro\.apilo\.com\/warehouse\/shipment\/new-for-order\/.+/;
|
||||
|
||||
if (pattern.test(currentUrl2)) {
|
||||
const portletBody = document.querySelector('.kt-portlet__body');
|
||||
if (portletBody) {
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'custom-button-container';
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.gap = '10px';
|
||||
buttonContainer.style.marginBottom = '15px';
|
||||
portletBody.parentNode.insertBefore(buttonContainer, portletBody);
|
||||
|
||||
const buttonP2D = createButton('Inpost P2D', '#007bff', 'P2D.inpost', 'RZE14N||RZE14N');
|
||||
const buttonD2D = createButton('Inpost D2D', '#ff7800', 'D2D.inpostkurier');
|
||||
const buttonD2P = createButton('Inpost D2P', '#28a745', 'D2P.inpost');
|
||||
const buttonP2P = createButton('Inpost P2P', '#ffc107', 'P2P.inpost', 'RZE14N||RZE14N');
|
||||
|
||||
buttonContainer.appendChild(buttonP2D);
|
||||
buttonContainer.appendChild(buttonD2D);
|
||||
buttonContainer.appendChild(buttonD2P);
|
||||
buttonContainer.appendChild(buttonP2P);
|
||||
}
|
||||
}
|
||||
|
||||
function createButton(text, backgroundColor, method, dropoffPoint = null) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = text;
|
||||
button.style.background = backgroundColor;
|
||||
button.style.display = 'inline-flex';
|
||||
button.style.width = '200px';
|
||||
button.style.height = '40px';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
button.style.color = '#FFF';
|
||||
button.style.border = 'none';
|
||||
button.style.cursor = 'pointer';
|
||||
|
||||
button.addEventListener('click', () => handleShipment(method, dropoffPoint));
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
async function handleShipment(method, dropoffPoint = null) {
|
||||
try {
|
||||
await selectPackageType();
|
||||
await setShipmentMethod(method);
|
||||
if (dropoffPoint) {
|
||||
await setDropoffPoint(dropoffPoint);
|
||||
}
|
||||
await setParcelWeight('1');
|
||||
await submitShipment();
|
||||
} catch (error) {
|
||||
console.error('Wystąpił błąd: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
function retryUntilSuccess(fn, interval = 500, retries = 10) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const attempt = async () => {
|
||||
try {
|
||||
const result = await fn();
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (retries === 0) {
|
||||
reject(err);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
retries--;
|
||||
attempt();
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
};
|
||||
attempt();
|
||||
});
|
||||
}
|
||||
|
||||
function selectPackageType() {
|
||||
return retryUntilSuccess(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const selectElement = document.getElementById('warehousebundle_shipment_packageType');
|
||||
if (selectElement) {
|
||||
selectElement.value = 'package';
|
||||
const event = document.createEvent('HTMLEvents');
|
||||
event.initEvent('change', true, false);
|
||||
selectElement.dispatchEvent(event);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nie znaleziono elementu packageType');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setShipmentMethod(method) {
|
||||
return retryUntilSuccess(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const methodElement = document.getElementById('warehousebundle_shipment_method');
|
||||
if (methodElement) {
|
||||
methodElement.value = method;
|
||||
const methodEvent = document.createEvent('HTMLEvents');
|
||||
methodEvent.initEvent('change', true, false);
|
||||
methodElement.dispatchEvent(methodEvent);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nie znaleziono elementu shipment_method');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setDropoffPoint(dropoffPoint) {
|
||||
return retryUntilSuccess(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dropoffPointElement = document.getElementById('warehousebundle_shipment_preferences_dropoffPoint');
|
||||
if (dropoffPointElement) {
|
||||
dropoffPointElement.value = dropoffPoint;
|
||||
const dropoffPointEvent = document.createEvent('HTMLEvents');
|
||||
dropoffPointEvent.initEvent('change', true, false);
|
||||
dropoffPointElement.dispatchEvent(dropoffPointEvent);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nie znaleziono elementu dropoffPoint');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setParcelWeight(weight) {
|
||||
return retryUntilSuccess(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const weightElement = document.getElementById('warehousebundle_shipment_shipmentParcels_0_weight');
|
||||
if (weightElement) {
|
||||
weightElement.value = weight;
|
||||
const weightEvent = document.createEvent('HTMLEvents');
|
||||
weightEvent.initEvent('change', true, false);
|
||||
weightElement.dispatchEvent(weightEvent);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nie znaleziono elementu shipmentParcels_0_weight');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function submitShipment() {
|
||||
return retryUntilSuccess(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const submitButton = document.getElementById('warehousebundle_shipment_buttons_submit');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nie znaleziono przycisku submit');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,38 @@ namespace admin\factory;
|
||||
use shop\Product;
|
||||
class ShopProduct
|
||||
{
|
||||
private static function seoLinkUsedByOtherProduct( int $product_id, string $lang_id, string $seo_link ): bool
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( !$seo_link )
|
||||
return false;
|
||||
|
||||
return (bool) $mdb -> count( 'pp_shop_products_langs', [
|
||||
'AND' => [
|
||||
'lang_id' => $lang_id,
|
||||
'seo_link' => $seo_link,
|
||||
'product_id[!]' => $product_id,
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
private static function removeConflictingRedirectSources( int $product_id, string $lang_id, string $from ): void
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( !$from )
|
||||
return;
|
||||
|
||||
$mdb -> delete( 'pp_redirects', [
|
||||
'AND' => [
|
||||
'from' => $from,
|
||||
'lang_id' => $lang_id,
|
||||
'product_id[!]' => $product_id,
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
// count_product
|
||||
static public function count_product( $where = null )
|
||||
{
|
||||
@@ -1010,18 +1042,30 @@ class ShopProduct
|
||||
|
||||
if ( $new_seo_link !== $current_seo_link and $current_seo_link != '' )
|
||||
{
|
||||
if ( !$mdb -> count( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) )
|
||||
if ( $mdb -> count( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) )
|
||||
$mdb -> delete( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] );
|
||||
|
||||
$mdb -> delete( 'pp_redirects', [
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'lang_id' => $lg['id'],
|
||||
'from' => $current_seo_link,
|
||||
'to[!]' => $new_seo_link,
|
||||
],
|
||||
] );
|
||||
|
||||
if ( !self::seoLinkUsedByOtherProduct( (int) $product_id, (string) $lg['id'], (string) $current_seo_link ) )
|
||||
{
|
||||
if ( $mdb -> count( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) )
|
||||
$mdb -> delete( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] );
|
||||
else
|
||||
self::removeConflictingRedirectSources( (int) $product_id, (string) $lg['id'], (string) $current_seo_link );
|
||||
|
||||
if ( !$mdb -> count( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) )
|
||||
{
|
||||
if ( \S::canAddRedirect( $current_seo_link, $new_seo_link ) )
|
||||
if ( \S::canAddRedirect( $current_seo_link, $new_seo_link, $lg['id'] ) )
|
||||
$mdb -> insert( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] );
|
||||
else
|
||||
$mdb -> delete( 'pp_redirects', [ 'product_id' => $product_id, 'lang_id' => $lg['id'] ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
$mdb -> delete( 'pp_redirects', [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => $lg['id'], 'from' => $current_seo_link ] ] );
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_shop_products_langs', [
|
||||
|
||||
@@ -49,46 +49,58 @@ class S
|
||||
return $parts;
|
||||
}
|
||||
|
||||
static function canAddRedirect( $from, $to )
|
||||
static function canAddRedirect( $from, $to, $lang_id = null )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$redirects = $mdb -> select( 'pp_redirects', '*' );
|
||||
if ( !$from or !$to or $from === $to )
|
||||
return false;
|
||||
|
||||
$where = [];
|
||||
if ( null !== $lang_id )
|
||||
$where['lang_id'] = $lang_id;
|
||||
|
||||
$redirects = $mdb -> select( 'pp_redirects', [ 'from', 'to' ], $where );
|
||||
|
||||
$redirectMap = [];
|
||||
foreach ( $redirects as $redirect )
|
||||
{
|
||||
$redirectMap[$redirect['from']] = $redirect['to'];
|
||||
if ( !isset( $redirectMap[$redirect['from']] ) )
|
||||
$redirectMap[$redirect['from']] = [];
|
||||
|
||||
if ( !in_array( $redirect['to'], $redirectMap[$redirect['from']], true ) )
|
||||
$redirectMap[$redirect['from']][] = $redirect['to'];
|
||||
}
|
||||
|
||||
// Dodaj nowe przekierowanie do mapy tymczasowo
|
||||
$redirectMap[$from] = $to;
|
||||
if ( !isset( $redirectMap[$from] ) )
|
||||
$redirectMap[$from] = [];
|
||||
|
||||
if ( !in_array( $to, $redirectMap[$from], true ) )
|
||||
$redirectMap[$from][] = $to;
|
||||
|
||||
// Funkcja do sprawdzania cyklu za pomocą DFS
|
||||
$visited = [];
|
||||
$stack = [];
|
||||
|
||||
function hasCycle($current, $target, &$redirectMap, &$visited)
|
||||
$stack = [ $to ];
|
||||
while ( !empty( $stack ) )
|
||||
{
|
||||
if ($current === $target) {
|
||||
return true;
|
||||
}
|
||||
$current = array_pop( $stack );
|
||||
|
||||
if (isset($visited[$current])) {
|
||||
return false;
|
||||
}
|
||||
if ( $current === $from )
|
||||
return false;
|
||||
|
||||
if ( isset( $visited[$current] ) )
|
||||
continue;
|
||||
|
||||
$visited[$current] = true;
|
||||
|
||||
if (isset($redirectMap[$current])) {
|
||||
return hasCycle($redirectMap[$current], $target, $redirectMap, $visited);
|
||||
if ( isset( $redirectMap[$current] ) )
|
||||
{
|
||||
foreach ( $redirectMap[$current] as $next )
|
||||
if ( !isset( $visited[$next] ) )
|
||||
$stack[] = $next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sprawdź, czy istnieje ścieżka z $newTo do $newFrom
|
||||
return !hasCycle($to, $from, $redirectMap, $visited);
|
||||
return true;
|
||||
}
|
||||
|
||||
static public function clear_redis_cache()
|
||||
@@ -1113,3 +1125,4 @@ class S
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,29 @@ class Layouts
|
||||
return $mdb -> get( 'pp_layouts', 'id', [ 'categories_default' => 1 ] );
|
||||
}
|
||||
|
||||
static public function default_layout()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$cacheHandler = new \CacheHandler();
|
||||
$cacheKey = "\front\factory\Layouts::default_layout";
|
||||
|
||||
$objectData = $cacheHandler -> get( $cacheKey );
|
||||
if ( $objectData )
|
||||
{
|
||||
$cachedLayout = @unserialize( $objectData );
|
||||
if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) )
|
||||
return $cachedLayout;
|
||||
|
||||
$cacheHandler -> delete( $cacheKey );
|
||||
}
|
||||
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] );
|
||||
$cacheHandler -> set( $cacheKey, $layout );
|
||||
|
||||
return $layout;
|
||||
}
|
||||
|
||||
static public function product_layout( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -16,18 +39,47 @@ class Layouts
|
||||
$cacheKey = "\front\factory\Layouts::product_layout:$product_id";
|
||||
|
||||
$objectData = $cacheHandler -> get( $cacheKey );
|
||||
|
||||
if ( !$objectData )
|
||||
if ( $objectData )
|
||||
{
|
||||
$layout = $mdb -> get( 'pp_layouts', [ '[><]pp_shop_products' => [ 'id' => 'layout_id' ] ], '*', [ 'pp_shop_products.id' => (int)$product_id ] );
|
||||
$cachedLayout = @unserialize( $objectData );
|
||||
if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) )
|
||||
return $cachedLayout;
|
||||
|
||||
$cacheHandler -> set( $cacheKey, $layout );
|
||||
$cacheHandler -> delete( $cacheKey );
|
||||
}
|
||||
|
||||
$layoutRows = $mdb -> query(
|
||||
"SELECT pp_layouts.*
|
||||
FROM pp_layouts
|
||||
JOIN pp_shop_products ON pp_layouts.id = pp_shop_products.layout_id
|
||||
WHERE pp_shop_products.id = " . (int)$product_id . "
|
||||
ORDER BY pp_layouts.id DESC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) )
|
||||
$layout = $layoutRows[0];
|
||||
else
|
||||
{
|
||||
return unserialize( $objectData );
|
||||
$layoutRows = $mdb -> query(
|
||||
"SELECT pp_layouts.*
|
||||
FROM pp_layouts
|
||||
JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id
|
||||
JOIN pp_shop_products_categories ON pp_shop_products_categories.category_id = pp_layouts_categories.category_id
|
||||
WHERE pp_shop_products_categories.product_id = " . (int)$product_id . "
|
||||
ORDER BY pp_shop_products_categories.o ASC, pp_layouts.id DESC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) )
|
||||
$layout = $layoutRows[0];
|
||||
else
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] );
|
||||
}
|
||||
|
||||
if ( !$layout )
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] );
|
||||
|
||||
$cacheHandler -> set( $cacheKey, $layout );
|
||||
|
||||
return $layout;
|
||||
}
|
||||
|
||||
@@ -62,21 +114,34 @@ class Layouts
|
||||
$cacheKey = "\front\factory\Layouts::category_layout:$category_id";
|
||||
|
||||
$objectData = $cacheHandler -> get( $cacheKey );
|
||||
|
||||
if ( !$objectData )
|
||||
if ( $objectData )
|
||||
{
|
||||
$layout = $mdb -> query( "SELECT pp_layouts.* FROM pp_layouts JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id WHERE pp_layouts_categories.category_id = " . (int)$category_id ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
if ( !$layout )
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] );
|
||||
$cachedLayout = @unserialize( $objectData );
|
||||
if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) )
|
||||
return $cachedLayout;
|
||||
|
||||
$cacheHandler -> set( $cacheKey, $layout[0] );
|
||||
$cacheHandler -> delete( $cacheKey );
|
||||
}
|
||||
|
||||
$layoutRows = $mdb -> query(
|
||||
"SELECT pp_layouts.*
|
||||
FROM pp_layouts
|
||||
JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id
|
||||
WHERE pp_layouts_categories.category_id = " . (int)$category_id . "
|
||||
ORDER BY pp_layouts.id DESC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) )
|
||||
$layout = $layoutRows[0];
|
||||
else
|
||||
{
|
||||
return unserialize( $objectData );
|
||||
}
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] );
|
||||
|
||||
return $layout[0];
|
||||
if ( !$layout )
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] );
|
||||
|
||||
$cacheHandler -> set( $cacheKey, $layout );
|
||||
|
||||
return $layout;
|
||||
}
|
||||
|
||||
static public function active_layout( $page_id )
|
||||
|
||||
@@ -34,6 +34,9 @@ class Site
|
||||
if ( \S::get( 'category' ) )
|
||||
$layout = \front\factory\Layouts::category_layout( \S::get( 'category' ) );
|
||||
|
||||
if ( !$layout and \S::get( 'module' ) )
|
||||
$layout = \front\factory\Layouts::default_layout();
|
||||
|
||||
if ( !$layout )
|
||||
$layout = \front\factory\Layouts::active_layout( $page['id'] );
|
||||
|
||||
|
||||
@@ -23,7 +23,27 @@ class Basket implements \ArrayAccess
|
||||
|
||||
foreach ( $basket as $key => $val )
|
||||
{
|
||||
$quantity_options = \shop\Product::get_product_permutation_quantity_options( $val['parent_id'] ? $val['parent_id'] : $val['product-id'], $val['attributes'][0] );
|
||||
$permutation = null;
|
||||
|
||||
if ( isset( $val['parent_id'] ) and (int)$val['parent_id'] and isset( $val['product-id'] ) )
|
||||
$permutation = \shop\Product::get_product_permutation_hash( (int)$val['product-id'] );
|
||||
|
||||
if ( !$permutation and isset( $val['attributes'] ) and is_array( $val['attributes'] ) and count( $val['attributes'] ) )
|
||||
$permutation = implode( '|', $val['attributes'] );
|
||||
|
||||
$quantity_options = \shop\Product::get_product_permutation_quantity_options(
|
||||
$val['parent_id'] ? $val['parent_id'] : $val['product-id'],
|
||||
$permutation
|
||||
);
|
||||
|
||||
if (
|
||||
(int)$basket[ $key ][ 'quantity' ] < 1
|
||||
and ( (int)$quantity_options['quantity'] > 0 or (int)$quantity_options['stock_0_buy'] === 1 )
|
||||
)
|
||||
{
|
||||
$basket[ $key ][ 'quantity' ] = 1;
|
||||
$result = true;
|
||||
}
|
||||
|
||||
if ( ( $val[ 'quantity' ] > $quantity_options['quantity'] ) and !$quantity_options['stock_0_buy'] )
|
||||
{
|
||||
|
||||
@@ -537,7 +537,7 @@ class Product implements \ArrayAccess
|
||||
global $mdb, $settings;
|
||||
|
||||
$cacheHandler = new \CacheHandler();
|
||||
$cacheKey = "\shop\Product::get_product_permutation_quantity_options:$product_id:$permutation";
|
||||
$cacheKey = "\shop\Product::get_product_permutation_quantity_options:v2:$product_id:$permutation";
|
||||
|
||||
$objectData = $cacheHandler -> get( $cacheKey );
|
||||
|
||||
@@ -546,10 +546,15 @@ class Product implements \ArrayAccess
|
||||
if ( $mdb -> count( 'pp_shop_products', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] ) )
|
||||
{
|
||||
$result['quantity'] = $mdb -> get( 'pp_shop_products', 'quantity', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] );
|
||||
$result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] );
|
||||
|
||||
if ( $result['quantity'] == null )
|
||||
{
|
||||
$result['quantity'] = $mdb -> get( 'pp_shop_products', 'quantity', [ 'id' => $product_id ] );
|
||||
$result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'id' => $product_id] );
|
||||
|
||||
if ( $result['stock_0_buy'] == null )
|
||||
$result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'id' => $product_id] );
|
||||
|
||||
$result['messages'] = $mdb -> get( 'pp_shop_products_langs', [ 'warehouse_message_zero', 'warehouse_message_nonzero' ], [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => 'pl' ] ] );
|
||||
|
||||
if ( !$result['messages']['warehouse_message_zero'] )
|
||||
@@ -560,7 +565,6 @@ class Product implements \ArrayAccess
|
||||
}
|
||||
else
|
||||
{
|
||||
$result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] );
|
||||
$result['messages'] = $mdb -> get( 'pp_shop_products_langs', [ 'warehouse_message_zero', 'warehouse_message_nonzero' ], [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => 'pl' ] ] );
|
||||
|
||||
if ( !$result['messages']['warehouse_message_zero'] )
|
||||
@@ -758,6 +762,13 @@ class Product implements \ArrayAccess
|
||||
return $mdb -> get( 'pp_shop_products', 'id', [ 'AND' => [ 'parent_id' => $parent_id, 'permutation_hash' => implode( '|', $attributes ) ] ] );
|
||||
}
|
||||
|
||||
// pobranie permutation_hash dla kombinacji produktu
|
||||
static public function get_product_permutation_hash( int $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> get( 'pp_shop_products', 'permutation_hash', [ 'id' => $product_id ] );
|
||||
}
|
||||
|
||||
// pobranie listy atrybutów z wybranymi wartościami
|
||||
static public function get_product_attributes( $products )
|
||||
{
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
<?php
|
||||
// geocode-cache.php
|
||||
|
||||
// ==== KONFIGURACJA ====
|
||||
const DB_DSN = 'mysql:host=localhost;dbname=host117523_shoppro;charset=utf8mb4';
|
||||
const DB_USER = 'host117523_shoppro';
|
||||
const DB_PASS = 'mhA9WCEXEnRfTtbN33hL';
|
||||
|
||||
// Klucz trzymaj w .env/konfiguracji, tu tylko przykład:
|
||||
const GOOGLE_GEOCODE_KEY = 'AIzaSyAViiTpMfvWkyL_Gb7QZLnGw_sjv_PKOIc';
|
||||
|
||||
// Domyślne opcje zapytań do Google – zawęź region/język wg potrzeb
|
||||
const GOOGLE_REGION = 'pl';
|
||||
const GOOGLE_LANGUAGE = 'pl';
|
||||
|
||||
// TTL cache w dniach
|
||||
const CACHE_TTL_DAYS_FORWARD = 365;
|
||||
const CACHE_TTL_DAYS_REVERSE = 365;
|
||||
|
||||
$ALLOWED_ORIGINS = [
|
||||
'https://twoja-domena.pl',
|
||||
'https://www.twoja-domena.pl',
|
||||
'http://localhost:3000',
|
||||
'https://shoppro.project-dc.pl',
|
||||
'https://moodo.pl'
|
||||
];
|
||||
|
||||
// ==== NAGŁÓWKI/CORS ====
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if ($origin && in_array($origin, $ALLOWED_ORIGINS, true)) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Vary: Origin');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||
|
||||
// ==== POMOCNICZE ====
|
||||
function normalize_query(string $q): string {
|
||||
$q = trim($q);
|
||||
$q = preg_replace('/\s+/u', ' ', $q);
|
||||
return mb_strtolower($q, 'UTF-8');
|
||||
}
|
||||
function now_mysql(): string { return date('Y-m-d H:i:s'); }
|
||||
function add_days(string $date, int $days): string { return date('Y-m-d H:i:s', strtotime("$date +$days days")); }
|
||||
|
||||
// Budowa stabilnego klucza cache
|
||||
function build_cache_key(?string $q, ?float $lat, ?float $lng): array {
|
||||
if ($q !== null && $q !== '') {
|
||||
$raw = $q;
|
||||
$norm = normalize_query($q);
|
||||
$mode = 'forward';
|
||||
} else {
|
||||
// reverse: lat,lng
|
||||
$latR = round((float)$lat, 6);
|
||||
$lngR = round((float)$lng, 6);
|
||||
$raw = sprintf('%F,%F', $lat, $lng);
|
||||
$norm = sprintf('latlng:%0.6f,%0.6f', $latR, $lngR);
|
||||
$mode = 'reverse';
|
||||
}
|
||||
return [
|
||||
'raw' => $raw,
|
||||
'norm' => $norm,
|
||||
'hash' => hash('sha256', $norm),
|
||||
'mode' => $mode,
|
||||
];
|
||||
}
|
||||
|
||||
// DB
|
||||
try {
|
||||
$pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'DB connection failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Wejście
|
||||
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
|
||||
$lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;
|
||||
$lng = isset($_GET['lng']) ? (float)$_GET['lng'] : null;
|
||||
|
||||
if ($q === '' && ($lat === null || $lng === null)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Provide q=address or lat & lng']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$key = build_cache_key($q, $lat, $lng);
|
||||
$now = now_mysql();
|
||||
|
||||
// ===== 1) CACHE LOOKUP (forward i reverse) =====
|
||||
$stmt = $pdo->prepare("SELECT * FROM geocode_cache WHERE query_hash = :h AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1");
|
||||
$stmt->execute([':h' => $key['hash']]);
|
||||
if ($row = $stmt->fetch()) {
|
||||
$pdo->prepare("UPDATE geocode_cache SET hits = hits + 1, updated_at = NOW() WHERE id = :id")->execute([':id' => $row['id']]);
|
||||
echo json_encode([
|
||||
'from_cache' => true,
|
||||
'source' => 'cache',
|
||||
'stale' => false,
|
||||
'lat' => (float)$row['lat'],
|
||||
'lng' => (float)$row['lng'],
|
||||
'formatted_address' => $row['formatted_address'],
|
||||
'place_id' => $row['place_id'],
|
||||
'provider' => $row['provider'],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===== 2) PYTANIE DO GOOGLE =====
|
||||
function google_request(array $params): array {
|
||||
$base = 'https://maps.googleapis.com/maps/api/geocode/json';
|
||||
$params['key'] = GOOGLE_GEOCODE_KEY;
|
||||
$params += ['language' => GOOGLE_LANGUAGE, 'region' => GOOGLE_REGION];
|
||||
$url = $base . '?' . http_build_query($params);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 3]);
|
||||
$res = curl_exec($ch); $err = curl_error($ch); curl_close($ch);
|
||||
if ($res === false) throw new RuntimeException('cURL error: ' . $err);
|
||||
$data = json_decode($res, true);
|
||||
if (!is_array($data)) throw new RuntimeException('Invalid JSON from Google');
|
||||
if (($data['status'] ?? '') !== 'OK' || empty($data['results'][0])) {
|
||||
$status = $data['status'] ?? 'UNKNOWN';
|
||||
throw new RuntimeException('Geocoding failed: ' . $status);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = ($key['mode'] === 'forward')
|
||||
? google_request(['address' => $q])
|
||||
: google_request(['latlng' => $lat . ',' . $lng]);
|
||||
|
||||
$top = $data['results'][0];
|
||||
$latV = (float)$top['geometry']['location']['lat'];
|
||||
$lngV = (float)$top['geometry']['location']['lng'];
|
||||
$fmt = (string)($top['formatted_address'] ?? '');
|
||||
$pid = (string)($top['place_id'] ?? '');
|
||||
|
||||
// ===== 3) ZAPIS DO CACHE (forward i reverse) =====
|
||||
$ttlDays = ($key['mode'] === 'forward') ? CACHE_TTL_DAYS_FORWARD : CACHE_TTL_DAYS_REVERSE;
|
||||
$expires = add_days($now, $ttlDays);
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO geocode_cache
|
||||
(query_hash, query_raw, query_norm, lat, lng, formatted_address, place_id, provider, hits, created_at, updated_at, expires_at, raw_json)
|
||||
VALUES (:h, :raw, :norm, :lat, :lng, :fmt, :pid, 'google', 1, :now, :now, :exp, :json)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
lat = VALUES(lat),
|
||||
lng = VALUES(lng),
|
||||
formatted_address = VALUES(formatted_address),
|
||||
place_id = VALUES(place_id),
|
||||
updated_at = VALUES(updated_at),
|
||||
expires_at = VALUES(expires_at),
|
||||
raw_json = VALUES(raw_json),
|
||||
hits = hits + 1");
|
||||
|
||||
$stmt->execute([
|
||||
':h' => $key['hash'],
|
||||
':raw' => $key['raw'],
|
||||
':norm'=> $key['norm'],
|
||||
':lat' => $latV,
|
||||
':lng' => $lngV,
|
||||
':fmt' => $fmt,
|
||||
':pid' => $pid,
|
||||
':now' => $now,
|
||||
':exp' => $expires,
|
||||
':json'=> json_encode($data, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'from_cache' => false,
|
||||
'source' => 'google',
|
||||
'stale' => false,
|
||||
'lat' => $latV,
|
||||
'lng' => $lngV,
|
||||
'formatted_address' => $fmt,
|
||||
'place_id' => $pid,
|
||||
'provider' => 'google',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// ===== 4) Fallback: zwróć najświeższe co mamy (też dla reverse) =====
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM geocode_cache WHERE query_hash = :h LIMIT 1");
|
||||
$stmt->execute([':h' => $key['hash']]);
|
||||
if ($row = $stmt->fetch()) {
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'from_cache' => true,
|
||||
'source' => 'cache',
|
||||
'stale' => true,
|
||||
'lat' => (float)$row['lat'],
|
||||
'lng' => (float)$row['lng'],
|
||||
'formatted_address' => $row['formatted_address'],
|
||||
'place_id' => $row['place_id'],
|
||||
'provider' => $row['provider'],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
} catch (Throwable $ignored) {}
|
||||
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
@@ -7,6 +7,22 @@
|
||||
unset( $price );
|
||||
unset( $price_new );
|
||||
$product = \shop\Product::getFromCache( (int)$position['product-id'], $this -> lang_id );
|
||||
|
||||
$permutation = null;
|
||||
if ( isset( $position['parent_id'] ) and (int)$position['parent_id'] and isset( $position['product-id'] ) )
|
||||
$permutation = \shop\Product::get_product_permutation_hash( (int)$position['product-id'] );
|
||||
|
||||
if ( !$permutation and isset( $position['attributes'] ) and is_array( $position['attributes'] ) and count( $position['attributes'] ) )
|
||||
$permutation = implode( '|', $position['attributes'] );
|
||||
|
||||
$quantity_options = \shop\Product::get_product_permutation_quantity_options(
|
||||
(int)( $position['parent_id'] ? $position['parent_id'] : $position['product-id'] ),
|
||||
$permutation
|
||||
);
|
||||
|
||||
$max_quantity = (int)$quantity_options['quantity'];
|
||||
if ( !$max_quantity and (int)$quantity_options['stock_0_buy'] )
|
||||
$max_quantity = 999;
|
||||
?>
|
||||
<div class="basket-product">
|
||||
<div class="image">
|
||||
@@ -72,7 +88,7 @@
|
||||
<a href="#" class="btn btn-default btn-minus" product-hash="<?= $position_hash; ?>">
|
||||
<i class="fa fa-minus"></i>
|
||||
</a>
|
||||
<input type="text" name="quantity" id="quantity" class="int-format form-control" min="1" max="<?= \shop\Product::get_product_quantity( (int)$position['product-id'] ); ?>" value="<?= $position['quantity']; ?>" product-hash="<?= $position_hash; ?>">
|
||||
<input type="text" name="quantity" id="quantity" class="int-format form-control" min="1" max="<?= $max_quantity; ?>" value="<?= $position['quantity']; ?>" product-hash="<?= $position_hash; ?>">
|
||||
<a href="#" class="btn btn-default btn-plus" product-hash="<?= $position_hash; ?>">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
@@ -111,4 +127,4 @@
|
||||
<div class="alert alert-danger">Brak produktów w koszyku</div>
|
||||
<? endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -321,17 +321,17 @@
|
||||
});
|
||||
};
|
||||
|
||||
if ( $( '#product #tab-0' ).visible() )
|
||||
if ( $( '#product #tab-0' ).is( ':visible' ) )
|
||||
$( '#product #tabs-menu #tab-link-0' ).addClass( 'current' );
|
||||
else
|
||||
$( '#product #tabs-menu #tab-link-0' ).removeClass( 'current' );
|
||||
|
||||
if ( $( '#product #tab-1' ).visible() )
|
||||
if ( $( '#product #tab-1' ).is( ':visible' ) )
|
||||
$( '#product #tabs-menu #tab-link-1' ).addClass( 'current' );
|
||||
else
|
||||
$( '#product #tabs-menu #tab-link-1' ).removeClass( 'current' );
|
||||
|
||||
if ( $( '#product #tab-2' ).visible() )
|
||||
if ( $( '#product #tab-2' ).is( ':visible' ) )
|
||||
$( '#product #tabs-menu #tab-link-2' ).addClass( 'current' );
|
||||
else
|
||||
$( '#product #tabs-menu #tab-link-2' ).removeClass( 'current' );
|
||||
|
||||
BIN
updates/0.20/ver_0.267.zip
Normal file
BIN
updates/0.20/ver_0.267.zip
Normal file
Binary file not shown.
2
updates/0.20/ver_0.267_files.txt
Normal file
2
updates/0.20/ver_0.267_files.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
F: ../apilo-bck
|
||||
F: ../geocode-cache.php
|
||||
11
updates/0.20/ver_0.267_sql.txt
Normal file
11
updates/0.20/ver_0.267_sql.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
DELETE r1 FROM pp_redirects r1
|
||||
INNER JOIN pp_redirects r2
|
||||
ON r1.`from` = r2.`from`
|
||||
AND (r1.lang_id <=> r2.lang_id)
|
||||
AND (
|
||||
IFNULL(r1.date_add, '1000-01-01 00:00:00') < IFNULL(r2.date_add, '1000-01-01 00:00:00')
|
||||
OR (
|
||||
IFNULL(r1.date_add, '1000-01-01 00:00:00') = IFNULL(r2.date_add, '1000-01-01 00:00:00')
|
||||
AND r1.id < r2.id
|
||||
)
|
||||
);
|
||||
@@ -1,3 +1,13 @@
|
||||
<b>ver. 0.267 - 13.02.2026</b><br />
|
||||
- FIX - front: poprawione dobieranie layoutu dla kategorii/produktu/koszyka i innych stron modułowych (fallback do layoutu domyślnego)
|
||||
- FIX - produkt/koszyk: poprawiona obsługa ilości dla kombinacji (stan 0 po dodaniu do koszyka, limit max, odczyt `stock_0_buy`)
|
||||
- FIX - produkt: usunięty błąd JS `TypeError: $(...).visible is not a function` (zamiana na `:visible`)
|
||||
- FIX - SEO redirecty produktów: blokada konfliktów po kopiowaniu URL oraz utwardzone wykrywanie pętli redirectów (`lang_id` + graf przejść)
|
||||
- UPDATE - admin: `input-switch` zapisuje wartość `on` (spójnie z obsługą pól checkbox w formularzach)
|
||||
- CLEANUP - usunięte pliki: `apilo-bck`, `geocode-cache.php`
|
||||
- UPDATE - testy: `OK (235 tests, 682 assertions)`
|
||||
- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.267.zip`, `ver_0.267_files.txt`, `ver_0.267_sql.txt`
|
||||
<hr>
|
||||
<b>ver. 0.266 - 13.02.2026</b><br />
|
||||
- NEW - migracja modulu `ShopCoupon` do architektury Domain + DI (`Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`)
|
||||
- UPDATE - modul `/admin/shop_coupon/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`
|
||||
@@ -462,4 +472,3 @@
|
||||
- FIX - poprawa adresu strony głównej
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 266;
|
||||
$current_ver = 267;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user