тут очень много с карточкой товара и импортом.

This commit is contained in:
Alisa
2026-06-05 18:50:38 +03:00
parent be883c5843
commit c08519263c
8 changed files with 379 additions and 69 deletions
+2 -2
View File
@@ -84,8 +84,8 @@ final class CatalogController
['title' => 'Каталог', 'url' => '/catalog'], ['title' => 'Каталог', 'url' => '/catalog'],
]; ];
if (!empty($product['category_id'])) { if (!empty($product['display_category_id'] ?? $product['category_id'] ?? null)) {
$breadcrumbs = $categoryRepository->breadcrumbs((int) $product['category_id']); $breadcrumbs = $categoryRepository->breadcrumbs((int) ($product['display_category_id'] ?? $product['category_id']));
} }
$breadcrumbs[] = ['title' => (string) $product['name']]; $breadcrumbs[] = ['title' => (string) $product['name']];
+25 -3
View File
@@ -50,13 +50,21 @@ final class ProductRepository extends BaseRepository
$placeholders = implode(',', array_fill(0, count($categoryIds), '?')); $placeholders = implode(',', array_fill(0, count($categoryIds), '?'));
$sql = $this->previewSelectSql( $sql = $this->previewSelectSql(
'AND ( 'AND (
p.category_id IN (' . $placeholders . ') EXISTS (
OR EXISTS (
SELECT 1 SELECT 1
FROM product_category_links pcl FROM product_category_links pcl
WHERE pcl.product_id = p.id WHERE pcl.product_id = p.id
AND pcl.category_id IN (' . $placeholders . ') AND pcl.category_id IN (' . $placeholders . ')
) )
OR (
p.category_id IN (' . $placeholders . ')
AND (p.legacy_path IS NULL OR p.legacy_path NOT LIKE \'/katalog/product/view/1/%\')
AND NOT EXISTS (
SELECT 1
FROM product_category_links pcl_any
WHERE pcl_any.product_id = p.id
)
)
) )
ORDER BY p.sort_order, p.name', ORDER BY p.sort_order, p.name',
$limit $limit
@@ -74,9 +82,23 @@ final class ProductRepository extends BaseRepository
public function findBySlug(string $slug): ?array public function findBySlug(string $slug): ?array
{ {
$statement = $this->pdo()->prepare( $statement = $this->pdo()->prepare(
'SELECT p.*, c.name AS category_name, c.slug AS category_slug 'SELECT
p.*,
COALESCE(linked_c.id, c.id) AS display_category_id,
COALESCE(linked_c.name, c.name) AS category_name,
COALESCE(linked_c.slug, c.slug) AS category_slug
FROM products p FROM products p
LEFT JOIN categories c ON c.id = p.category_id LEFT JOIN categories c ON c.id = p.category_id
LEFT JOIN categories linked_c
ON linked_c.id = (
SELECT pcl.category_id
FROM product_category_links pcl
INNER JOIN categories lc ON lc.id = pcl.category_id
WHERE pcl.product_id = p.id
AND lc.is_active = 1
ORDER BY lc.parent_id IS NULL, pcl.sort_order, pcl.category_id
LIMIT 1
)
WHERE p.slug = :slug WHERE p.slug = :slug
AND p.is_published = 1 AND p.is_published = 1
AND p.is_available = 1 AND p.is_available = 1
+23
View File
@@ -170,6 +170,28 @@ function execute_old_site_import_batch(string $sqlPath, string $progressPath, ar
]; ];
} }
function normalize_old_site_product_categories(PDO $pdo): void
{
$pdo->exec(
"UPDATE products p
SET p.category_id = (
SELECT pcl.category_id
FROM product_category_links pcl
INNER JOIN categories c ON c.id = pcl.category_id
WHERE pcl.product_id = p.id
AND c.is_active = 1
ORDER BY c.parent_id IS NULL, pcl.sort_order, pcl.category_id
LIMIT 1
)
WHERE p.legacy_path LIKE '/katalog/product/view/1/%'
AND EXISTS (
SELECT 1
FROM product_category_links pcl_exists
WHERE pcl_exists.product_id = p.id
)"
);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try { try {
$action = (string) ($_POST['action'] ?? 'start'); $action = (string) ($_POST['action'] ?? 'start');
@@ -186,6 +208,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$progress = execute_old_site_import_batch($sqlPath, $progressPath, $progress); $progress = execute_old_site_import_batch($sqlPath, $progressPath, $progress);
if (!empty($progress['done'])) { if (!empty($progress['done'])) {
normalize_old_site_product_categories(Database::pdo());
$message = 'Импорт выполнен. SQL-команд выполнено: ' . (int) $progress['executed'] . '.'; $message = 'Импорт выполнен. SQL-команд выполнено: ' . (int) $progress['executed'] . '.';
} else { } else {
$message = 'Импорт продолжается: ' . (float) $progress['percent'] . '%, команд за этот шаг: ' . (int) $progress['batch_executed'] . '.'; $message = 'Импорт продолжается: ' . (float) $progress['percent'] . '%, команд за этот шаг: ' . (int) $progress['batch_executed'] . '.';
+168 -21
View File
@@ -1007,7 +1007,13 @@ a:hover {
font-weight: 700; font-weight: 700;
} }
.product-grid, .product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
align-items: stretch;
}
.admin-grid { .admin-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
@@ -1024,6 +1030,13 @@ a:hover {
box-shadow: 0 10px 24px rgba(22, 60, 43, 0.05); box-shadow: 0 10px 24px rgba(22, 60, 43, 0.05);
} }
.product-card {
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
}
.product-card-image { .product-card-image {
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -1044,6 +1057,23 @@ a:hover {
transform: scale(1.025); transform: scale(1.025);
} }
.product-card-image-placeholder {
display: grid;
place-items: center;
aspect-ratio: 4 / 3;
color: var(--brand-green);
font-weight: 900;
background:
linear-gradient(135deg, rgba(23, 61, 44, 0.08), rgba(196, 61, 49, 0.08)),
#eef4ea;
}
.product-card-image-placeholder span {
display: inline-flex;
border-bottom: 2px solid var(--brand-red);
padding-bottom: 3px;
}
.product-card p { .product-card p {
margin: 0 0 10px; margin: 0 0 10px;
font-size: 14px; font-size: 14px;
@@ -1051,15 +1081,33 @@ a:hover {
} }
.product-card .product-category { .product-card .product-category {
display: flex;
align-items: flex-start;
overflow: hidden;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 12px; font-size: 12px;
line-height: 1.2;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
color: #7a856f; color: #7a856f;
} }
.product-card h3 {
display: -webkit-box;
overflow: hidden;
min-height: 50px;
margin-bottom: 12px;
line-height: 1.22;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.price-line { .price-line {
display: block; display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: baseline;
min-height: 22px;
margin-bottom: 6px; margin-bottom: 6px;
font-size: 14px; font-size: 14px;
color: #2d372f; color: #2d372f;
@@ -1067,25 +1115,58 @@ a:hover {
.package-price-line { .package-price-line {
position: relative; position: relative;
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: minmax(0, 1fr) auto 22px;
gap: 4px 7px; gap: 6px;
align-items: center; align-items: center;
min-height: 30px;
border-top: 1px solid #e3e8de;
padding-top: 10px;
font-size: 14px;
}
.package-price-label {
min-width: 0;
color: #2d372f;
}
.package-price-values {
display: inline-flex;
gap: 6px;
align-items: baseline;
justify-content: flex-end;
min-width: 0;
color: var(--brand-green);
white-space: nowrap;
}
.package-price-note {
display: block;
margin: 3px 0 8px;
font-size: 12px;
line-height: 1.25;
color: var(--muted);
} }
.old-price { .old-price {
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 0;
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
color: #8b9485; color: #8b9485;
text-decoration: line-through; text-decoration: line-through;
white-space: nowrap;
} }
.variant-choice { .variant-choice {
display: grid; display: grid;
align-self: start;
gap: 7px; gap: 7px;
margin-top: 12px; min-height: 76px;
margin: 0 0 12px;
border-top: 0;
border-bottom: 1px solid #edf1e9;
padding-bottom: 10px;
} }
.variant-choice > span { .variant-choice > span {
@@ -1098,20 +1179,27 @@ a:hover {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
align-items: flex-start;
min-height: 32px;
} }
.variant-choice-button { .variant-choice-button {
appearance: none; appearance: none;
max-width: 100%;
box-sizing: border-box;
min-height: 30px; min-height: 30px;
border: 1px solid #cfd8c9; border: 1px solid #cfd8c9;
border-radius: 7px; border-radius: 7px;
padding: 6px 9px; padding: 6px 9px;
font: inherit; font: inherit;
font-size: 13px; font-size: 12px;
line-height: 1.15;
font-weight: 800; font-weight: 800;
color: var(--brand-green); color: var(--brand-green);
background: #ffffff; background: #ffffff;
cursor: pointer; cursor: pointer;
overflow-wrap: anywhere;
transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
} }
.variant-choice-button:hover, .variant-choice-button:hover,
@@ -1123,11 +1211,12 @@ a:hover {
.add-to-cart-button { .add-to-cart-button {
appearance: none; appearance: none;
align-self: end;
width: 100%; width: 100%;
min-height: 40px; min-height: 40px;
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
margin-top: 14px; margin-top: 0;
padding: 10px 12px; padding: 10px 12px;
font: inherit; font: inherit;
font-size: 14px; font-size: 14px;
@@ -1141,11 +1230,58 @@ a:hover {
background: #102f21; background: #102f21;
} }
.product-card-actions {
display: grid;
grid-template-columns: minmax(96px, 0.44fr) minmax(0, 1fr);
gap: 8px;
align-items: stretch;
min-height: 40px;
margin-top: auto;
}
.quantity-stepper {
display: grid;
grid-template-columns: 30px minmax(32px, 1fr) 30px;
min-width: 0;
overflow: hidden;
border: 1px solid #cfd8c9;
border-radius: 8px;
background: #ffffff;
}
.quantity-stepper button,
.quantity-stepper input {
min-width: 0;
border: 0;
font: inherit;
font-size: 14px;
font-weight: 900;
text-align: center;
color: var(--brand-green);
background: transparent;
}
.quantity-stepper button {
cursor: pointer;
}
.quantity-stepper button:hover {
color: #ffffff;
background: var(--brand-green);
}
.quantity-stepper input {
border-inline: 1px solid #e3e8de;
padding: 0 3px;
}
.price-tier-trigger { .price-tier-trigger {
appearance: none; appearance: none;
display: inline-grid; display: none;
place-items: center; place-items: center;
justify-self: end;
width: 20px; width: 20px;
min-width: 20px;
height: 20px; height: 20px;
border: 1px solid #b9c6b1; border: 1px solid #b9c6b1;
border-radius: 50%; border-radius: 50%;
@@ -1153,11 +1289,16 @@ a:hover {
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
font-weight: 900; font-weight: 900;
line-height: 1;
color: var(--brand-green); color: var(--brand-green);
background: #ffffff; background: #ffffff;
cursor: help; cursor: help;
} }
body[data-payment-mode="cash"] .price-tier-trigger {
display: inline-grid;
}
.cart-link span { .cart-link span {
display: inline-grid; display: inline-grid;
place-items: center; place-items: center;
@@ -1174,9 +1315,9 @@ a:hover {
.price-tier-popover { .price-tier-popover {
position: absolute; position: absolute;
left: 16px;
right: 16px; right: 16px;
bottom: 58px; bottom: 62px;
left: 16px;
z-index: 8; z-index: 8;
display: grid; display: grid;
gap: 9px; gap: 9px;
@@ -1188,20 +1329,17 @@ a:hover {
box-shadow: 0 18px 44px rgba(23, 61, 44, 0.18); box-shadow: 0 18px 44px rgba(23, 61, 44, 0.18);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(6px);
pointer-events: none; pointer-events: none;
transform: translateY(6px);
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease; transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease;
} }
.product-card { body[data-payment-mode="cash"] .package-price-line:has(.price-tier-trigger:hover) + .price-tier-popover,
position: relative; body[data-payment-mode="cash"] .price-tier-popover:hover {
}
.product-card:hover .price-tier-popover,
.product-card:focus-within .price-tier-popover {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
pointer-events: auto;
} }
.price-tier-popover strong { .price-tier-popover strong {
@@ -1218,7 +1356,7 @@ a:hover {
.price-tier-popover div { .price-tier-popover div {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 10px; gap: 8px;
align-items: baseline; align-items: baseline;
} }
@@ -1226,6 +1364,7 @@ a:hover {
font-size: 12px; font-size: 12px;
line-height: 1.25; line-height: 1.25;
color: var(--muted); color: var(--muted);
white-space: nowrap;
} }
.price-tier-popover dd { .price-tier-popover dd {
@@ -1320,7 +1459,7 @@ a:hover {
.variant-row { .variant-row {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(130px, 0.7fr) minmax(110px, 0.55fr) minmax(120px, 0.55fr); grid-template-columns: minmax(0, 1.2fr) minmax(130px, 0.7fr) minmax(110px, 0.55fr) minmax(220px, 0.75fr);
gap: 12px; gap: 12px;
align-items: center; align-items: center;
border: 1px solid var(--line); border: 1px solid var(--line);
@@ -1329,6 +1468,10 @@ a:hover {
background: #f8faf5; background: #f8faf5;
} }
.variant-row .product-card-actions {
margin-top: 0;
}
.variant-row b { .variant-row b {
color: var(--brand-green); color: var(--brand-green);
text-align: right; text-align: right;
@@ -2338,6 +2481,10 @@ a:hover {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.product-card-actions {
grid-template-columns: 1fr;
}
.variant-row b { .variant-row b {
text-align: left; text-align: left;
} }
+8 -1
View File
@@ -62,7 +62,14 @@ $mainImage = $product['main_image_path'] ?? '';
<?php endif; ?> <?php endif; ?>
<span data-dynamic-package-price data-base-package-price="<?= e((string) $variant['package_price']) ?>"><?= e(money_label($variant['package_price'])) ?></span> <span data-dynamic-package-price data-base-package-price="<?= e((string) $variant['package_price']) ?>"><?= e(money_label($variant['package_price'])) ?></span>
</b> </b>
<button class="add-to-cart-button" type="button">В корзину</button> <div class="product-card-actions">
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
</div>
<button class="add-to-cart-button" type="button">В корзину</button>
</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>
+71 -14
View File
@@ -266,8 +266,7 @@ try {
const tierValues = (base) => ({ const tierValues = (base) => ({
base: base, base: base,
cash20: base * 0.95, cash20: base * 0.95,
cash50: base * 0.93, cash50: base * 0.93
invoice: base * 1.08
}); });
const updateDynamicPrices = (scope) => { const updateDynamicPrices = (scope) => {
@@ -287,7 +286,7 @@ try {
}; };
const updateCardPrices = (card) => { const updateCardPrices = (card) => {
const packageNode = card.querySelector('[data-package-price]'); const packageNode = card.querySelector('[data-dynamic-package-price]');
updateDynamicPrices(card); updateDynamicPrices(card);
if (packageNode) { if (packageNode) {
@@ -297,12 +296,10 @@ try {
const tierBase = card.querySelector('[data-tier-base]'); const tierBase = card.querySelector('[data-tier-base]');
const tierCash20 = card.querySelector('[data-tier-cash-20]'); const tierCash20 = card.querySelector('[data-tier-cash-20]');
const tierCash50 = card.querySelector('[data-tier-cash-50]'); const tierCash50 = card.querySelector('[data-tier-cash-50]');
const tierInvoice = card.querySelector('[data-tier-invoice]');
if (tierBase) tierBase.textContent = formatRub(tiers.base); if (tierBase) tierBase.textContent = formatRub(tiers.base);
if (tierCash20) tierCash20.textContent = formatRub(tiers.cash20); if (tierCash20) tierCash20.textContent = formatRub(tiers.cash20);
if (tierCash50) tierCash50.textContent = formatRub(tiers.cash50); if (tierCash50) tierCash50.textContent = formatRub(tiers.cash50);
if (tierInvoice) tierInvoice.textContent = formatRub(tiers.invoice);
} }
}; };
@@ -378,6 +375,8 @@ try {
const updateModeButtons = () => { const updateModeButtons = () => {
const payment = getPaymentMode(); const payment = getPaymentMode();
document.body.dataset.paymentMode = payment;
document.querySelectorAll('[data-payment-mode]').forEach((button) => { document.querySelectorAll('[data-payment-mode]').forEach((button) => {
button.classList.toggle('is-active', button.dataset.paymentMode === payment); button.classList.toggle('is-active', button.dataset.paymentMode === payment);
}); });
@@ -425,6 +424,37 @@ try {
window.RybStockPricing.setPaymentMode(modeButton.dataset.paymentMode || 'cash'); window.RybStockPricing.setPaymentMode(modeButton.dataset.paymentMode || 'cash');
}); });
document.addEventListener('click', (event) => {
const quantityButton = event.target.closest('[data-quantity-minus], [data-quantity-plus]');
if (!quantityButton) {
return;
}
const control = quantityButton.closest('[data-quantity-control]');
const input = control?.querySelector('[data-quantity-input]');
if (!input) {
return;
}
const current = Math.max(1, parseInt(input.value || '1', 10) || 1);
const next = quantityButton.matches('[data-quantity-plus]')
? current + 1
: Math.max(1, current - 1);
input.value = String(next);
});
document.addEventListener('input', (event) => {
const input = event.target.closest('[data-quantity-input]');
if (!input) {
return;
}
input.value = String(Math.max(1, parseInt(input.value.replace(/\D/g, '') || '1', 10) || 1));
});
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
const addButton = event.target.closest('.add-to-cart-button'); const addButton = event.target.closest('.add-to-cart-button');
@@ -435,19 +465,21 @@ try {
const productScope = addButton.closest('.price-aware'); const productScope = addButton.closest('.price-aware');
const row = addButton.closest('.variant-row'); const row = addButton.closest('.variant-row');
const variantButton = productScope?.querySelector('.variant-choice-button.is-active'); const variantButton = productScope?.querySelector('.variant-choice-button.is-active');
const packagePriceNode = row?.querySelector('[data-dynamic-package-price]') || productScope?.querySelector('[data-package-price]'); const packagePriceNode = row?.querySelector('[data-dynamic-package-price]') || productScope?.querySelector('[data-dynamic-package-price]');
const quantityInput = row?.querySelector('[data-quantity-input]') || productScope?.querySelector('[data-quantity-input]');
if (!productScope || !packagePriceNode) { if (!productScope || !packagePriceNode) {
return; return;
} }
const addQuantity = Math.max(1, parseInt(quantityInput?.value || '1', 10) || 1);
const variantId = row?.dataset.variantId || variantButton?.dataset.variantId || 'default'; const variantId = row?.dataset.variantId || variantButton?.dataset.variantId || 'default';
const key = String(productScope.dataset.productId || '') + ':' + variantId; const key = String(productScope.dataset.productId || '') + ':' + variantId;
const items = getCartItems(); const items = getCartItems();
const existing = items.find((item) => item.key === key); const existing = items.find((item) => item.key === key);
if (existing) { if (existing) {
existing.quantity = Number(existing.quantity || 0) + 1; existing.quantity = Number(existing.quantity || 0) + addQuantity;
} else { } else {
items.push({ items.push({
key, key,
@@ -459,7 +491,7 @@ try {
variantName: row?.dataset.variantName || variantButton?.dataset.variantName || 'Фасовка', variantName: row?.dataset.variantName || variantButton?.dataset.variantName || 'Фасовка',
basePackagePrice: Number(packagePriceNode.dataset.basePackagePrice || 0), basePackagePrice: Number(packagePriceNode.dataset.basePackagePrice || 0),
baseUnitPrice: Number(row?.dataset.baseUnitPrice || variantButton?.dataset.baseUnitPrice || 0), baseUnitPrice: Number(row?.dataset.baseUnitPrice || variantButton?.dataset.baseUnitPrice || 0),
quantity: 1 quantity: addQuantity
}); });
} }
@@ -526,15 +558,23 @@ try {
}); });
const unitNode = card.querySelector('[data-unit-label]'); const unitNode = card.querySelector('[data-unit-label]');
const unitPriceNode = card.querySelector('[data-price-unit]'); const unitPriceNode = card.querySelector('[data-dynamic-unit-price]');
const packagePriceNode = card.querySelector('[data-package-price]'); const packagePriceNode = card.querySelector('[data-dynamic-package-price]');
const oldPriceNode = card.querySelector('[data-old-price]'); const packageLabelNode = card.querySelector('[data-package-label-display]');
const packageLabelNoteNode = card.querySelector('[data-package-label-note]');
const unitLabelNoteNode = card.querySelector('[data-unit-label-note]');
const oldPriceNode = card.querySelector('[data-old-package-price-display]');
const oldUnitPriceNode = card.querySelector('[data-old-unit-price-display]');
if (unitNode) { if (unitNode) {
unitNode.textContent = button.dataset.unit || 'ед.'; unitNode.textContent = button.dataset.unit || 'ед.';
} }
if (unitPriceNode && button.dataset.priceUnit) { if (unitLabelNoteNode) {
unitLabelNoteNode.textContent = button.dataset.unit || 'ед.';
}
if (unitPriceNode) {
unitPriceNode.dataset.baseUnitPrice = button.dataset.baseUnitPrice || ''; unitPriceNode.dataset.baseUnitPrice = button.dataset.baseUnitPrice || '';
} }
@@ -542,9 +582,26 @@ try {
packagePriceNode.dataset.basePackagePrice = button.dataset.basePackagePrice || ''; packagePriceNode.dataset.basePackagePrice = button.dataset.basePackagePrice || '';
} }
if (packageLabelNode) {
packageLabelNode.textContent = button.dataset.variantPackageLabel || button.dataset.variantName || 'Фасовка';
}
if (packageLabelNoteNode) {
packageLabelNoteNode.textContent = button.dataset.variantPackageLabel || button.dataset.variantName || 'Фасовка';
}
if (oldPriceNode) { if (oldPriceNode) {
oldPriceNode.textContent = button.dataset.oldPrice || ''; const oldPackagePrice = button.dataset.oldPrice || '';
oldPriceNode.hidden = !button.dataset.oldPrice; oldPriceNode.textContent = oldPackagePrice;
oldPriceNode.hidden = oldPackagePrice === '';
}
if (oldUnitPriceNode) {
const oldUnitPrice = Number(button.dataset.oldUnitPrice || 0);
const baseUnitPrice = Number(button.dataset.baseUnitPrice || 0);
const hasOldUnitPrice = oldUnitPrice > baseUnitPrice;
oldUnitPriceNode.textContent = hasOldUnitPrice ? formatRub(oldUnitPrice) : '';
oldUnitPriceNode.hidden = !hasOldUnitPrice;
} }
updateCardPrices(card); updateCardPrices(card);
+81 -27
View File
@@ -9,11 +9,26 @@ $activeVariant = $variants[0] ?? [
'unit' => $product['unit'] ?? '', 'unit' => $product['unit'] ?? '',
'price_per_unit' => $product['price_per_unit'] ?? null, 'price_per_unit' => $product['price_per_unit'] ?? null,
'package_price' => $product['package_price'] ?? null, 'package_price' => $product['package_price'] ?? null,
'package_quantity' => $product['package_quantity'] ?? 0,
'old_package_price' => $product['old_package_price'] ?? null, 'old_package_price' => $product['old_package_price'] ?? null,
]; ];
$cardId = 'product-card-' . (int) ($product['id'] ?? 0); $cardId = 'product-card-' . (int) ($product['id'] ?? 0);
$activePackagePrice = (float) ($activeVariant['package_price'] ?? 0); $activePackagePrice = (float) ($activeVariant['package_price'] ?? 0);
$activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $activeVariant['price_per_unit']; $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $activeVariant['price_per_unit'];
$activePackageQuantity = (float) ($activeVariant['package_quantity'] ?? 0);
$activeVariantOldPackagePrice = $activeVariant['old_package_price'] ?? null;
$activeOldPackagePrice = (
$activeVariantOldPackagePrice !== null
&& (float) $activeVariantOldPackagePrice > $activePackagePrice
)
? (float) $activeVariantOldPackagePrice
: null;
$activeOldUnitPrice = ($activeOldPackagePrice !== null && $activePackageQuantity > 0 && $activeUnitPrice !== null)
? $activeOldPackagePrice / $activePackageQuantity
: null;
if ($activeOldUnitPrice !== null && $activeOldUnitPrice <= $activeUnitPrice) {
$activeOldUnitPrice = null;
}
?> ?>
<article <article
class="product-card price-aware" class="product-card price-aware"
@@ -27,38 +42,37 @@ $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $a
<a class="product-card-image" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>"> <a class="product-card-image" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
<img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>"> <img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>">
</a> </a>
<?php else: ?>
<a class="product-card-image product-card-image-placeholder" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
<span>Рыбсток</span>
</a>
<?php endif; ?> <?php endif; ?>
<p class="product-category"><?= e($product['category_display_name'] ?? $product['category_name'] ?? 'Каталог') ?></p> <p class="product-category"><?= e($product['category_display_name'] ?? $product['category_name'] ?? 'Каталог') ?></p>
<h3><a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a></h3> <h3><a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a></h3>
<?php if ($activeVariant['price_per_unit'] !== null): ?>
<span class="price-line">Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>: <b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b></span>
<?php endif; ?>
<?php if ($activeVariant['package_price'] !== null): ?>
<strong class="package-price-line">
<span class="old-price" data-old-price<?= empty($activeVariant['old_package_price']) ? ' hidden' : '' ?>><?= e(money_label($activeVariant['old_package_price'] ?? null)) ?></span>
За упаковку: <span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
</strong>
<?php endif; ?>
<div class="price-tier-popover" role="tooltip">
<strong>Варианты цены</strong>
<dl>
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activePackagePrice)) ?></dd></div>
<div><dt>Цена при сумме заказа от 20 000р. наличными</dt><dd data-tier-cash-20><?= e(money_label(floor($activePackagePrice * 0.95))) ?></dd></div>
<div><dt>Цена при сумме заказа от 50 000р. наличными</dt><dd data-tier-cash-50><?= e(money_label(floor($activePackagePrice * 0.93))) ?></dd></div>
<div><dt>Оплата по расчетному счету</dt><dd data-tier-invoice><?= e(money_label(floor($activePackagePrice * 1.08))) ?></dd></div>
</dl>
</div>
<?php if ($variants !== []): ?> <?php if ($variants !== []): ?>
<div class="variant-choice" aria-label="Выбор фасовки"> <div class="variant-choice" aria-label="Выбор фасовки">
<span>Фасовка:</span> <span>Доступные фасовки:</span>
<div class="variant-choice-list"> <div class="variant-choice-list">
<?php foreach ($variants as $index => $variant): ?> <?php foreach ($variants as $index => $variant): ?>
<?php
$variantPackagePrice = (float) ($variant['package_price'] ?? 0);
$variantUnitPrice = $variant['price_per_unit'] === null ? null : (float) $variant['price_per_unit'];
$variantPackageQuantity = (float) ($variant['package_quantity'] ?? 0);
$variantOldPackagePrice = (
!empty($variant['old_package_price'])
&& (float) $variant['old_package_price'] > $variantPackagePrice
)
? (float) $variant['old_package_price']
: null;
$variantOldUnitPrice = ($variantOldPackagePrice !== null && $variantPackageQuantity > 0 && $variantUnitPrice !== null)
? $variantOldPackagePrice / $variantPackageQuantity
: null;
if ($variantOldUnitPrice !== null && $variantOldUnitPrice <= $variantUnitPrice) {
$variantOldUnitPrice = null;
}
?>
<button <button
class="variant-choice-button<?= $index === 0 ? ' is-active' : '' ?>" class="variant-choice-button<?= $index === 0 ? ' is-active' : '' ?>"
type="button" type="button"
@@ -67,10 +81,12 @@ $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $a
data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>" data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>"
data-unit="<?= e(unit_label((string) $variant['unit'])) ?>" data-unit="<?= e(unit_label((string) $variant['unit'])) ?>"
data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>" data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>"
data-old-unit-price="<?= e($variantOldUnitPrice === null ? '' : (string) $variantOldUnitPrice) ?>"
data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>" data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>"
data-price-unit="<?= e(money_label($variant['price_per_unit'])) ?>" data-variant-package-label="<?= e((string) ($variant['name'] ?? '')) ?>"
data-package-price="<?= e(money_label($variant['package_price'])) ?>" data-variant-unit-price-label="<?= e(money_label($variant['price_per_unit'])) ?>"
data-old-price="<?= e(money_label($variant['old_package_price'] ?? null)) ?>" data-variant-package-price-label="<?= e(money_label($variant['package_price'])) ?>"
data-old-price="<?= e($variantOldPackagePrice === null ? '' : money_label($variantOldPackagePrice)) ?>"
> >
<?= e((string) $variant['name']) ?> <?= e((string) $variant['name']) ?>
</button> </button>
@@ -79,5 +95,43 @@ $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $a
</div> </div>
<?php endif; ?> <?php endif; ?>
<button class="add-to-cart-button" type="button">В корзину</button> <?php if ($activeVariant['price_per_unit'] !== null): ?>
<span class="price-line">
Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>:
<span class="old-price" data-old-unit-price-display<?= $activeOldUnitPrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldUnitPrice)) ?></span>
<b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b>
</span>
<?php endif; ?>
<?php if ($activeVariant['package_price'] !== null): ?>
<strong class="package-price-line">
<span class="package-price-label">За фасовку <span data-package-label-display><?= e((string) $activeVariant['name']) ?></span>:</span>
<span class="package-price-values">
<span class="old-price" data-old-package-price-display<?= $activeOldPackagePrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldPackagePrice)) ?></span>
<span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
</span>
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
</strong>
<?php if ($activeVariant['price_per_unit'] !== null): ?>
<span class="package-price-note">Цена за <span data-unit-label-note><?= e(unit_label((string) $activeVariant['unit'])) ?></span> × <span data-package-label-note><?= e((string) $activeVariant['name']) ?></span></span>
<?php endif; ?>
<?php endif; ?>
<div class="price-tier-popover" role="tooltip">
<strong>Варианты цены за наличный расчет:</strong>
<dl>
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activePackagePrice)) ?></dd></div>
<div><dt>При заказе от 20 000р.</dt><dd data-tier-cash-20><?= e(money_label(floor($activePackagePrice * 0.95))) ?></dd></div>
<div><dt>При заказе от 50 000р.</dt><dd data-tier-cash-50><?= e(money_label(floor($activePackagePrice * 0.93))) ?></dd></div>
</dl>
</div>
<div class="product-card-actions">
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
</div>
<button class="add-to-cart-button" type="button">В корзину</button>
</div>
</article> </article>
+1 -1
View File
@@ -383,7 +383,7 @@
15. Нужно ли сохранять расчет доставки в заказе или только показывать клиенту предварительно? 15. Нужно ли сохранять расчет доставки в заказе или только показывать клиенту предварительно?
16. Нужен ли ручной пересчет доставки администратором в заказе? 16. Нужен ли ручной пересчет доставки администратором в заказе?
17. Как учитывать подъем 390 руб.: отдельной галочкой в корзине или только текстом? 17. Как учитывать подъем 390 руб.: отдельной галочкой в корзине или только текстом?
18. Нужен ли выбор даты доставки в календаре? 18. Нужен ли выбор даты доставки в календаре..S?
19. Нужно ли запрещать выбор выходных и праздников в календаре? 19. Нужно ли запрещать выбор выходных и праздников в календаре?
20. Где брать производственный календарь РФ: встроенный список, API или ручная настройка в админке? 20. Где брать производственный календарь РФ: встроенный список, API или ручная настройка в админке?
21. Какой должен быть внешний стиль: более премиальный, складской/оптовый, домашний семейный или нейтральный продуктовый? 21. Какой должен быть внешний стиль: более премиальный, складской/оптовый, домашний семейный или нейтральный продуктовый?