feat: скидки на товары и редактирование промокодов в админке
Цена со скидкой и срок акции на товаре; отображение в каталоге и корзине. Улучшенный UI промокодов с редактированием. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -99,12 +99,20 @@ router.get(
|
||||
LEFT JOIN categories c ON c.id = p.category_id
|
||||
ORDER BY p.id`
|
||||
);
|
||||
const productPrice = require('../utils/productPrice');
|
||||
res.render('admin/products', {
|
||||
title: 'Товары',
|
||||
products,
|
||||
formatPrice,
|
||||
isSaleActive: productPrice.isSaleActive,
|
||||
effectivePrice: productPrice.getEffectivePriceCents,
|
||||
salePercent: productPrice.salePercent,
|
||||
stockUpdated: req.query.stock_updated === '1',
|
||||
notified: req.query.notified ? parseInt(req.query.notified, 10) : 0,
|
||||
pricingUpdated: req.query.pricing_updated === '1',
|
||||
pricingError: req.query.pricing_error
|
||||
? decodeURIComponent(String(req.query.pricing_error))
|
||||
: null,
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -135,6 +143,114 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/products/:id/pricing',
|
||||
asyncHandler(async (req, res) => {
|
||||
const productId = parseInt(req.params.id, 10);
|
||||
const priceRub = parseFloat(String(req.body.price_rub || '').replace(',', '.'));
|
||||
const saleRubRaw = String(req.body.sale_price_rub ?? '').trim();
|
||||
const clearSale = req.body.clear_sale === '1';
|
||||
|
||||
if (clearSale) {
|
||||
const price_cents = Number.isFinite(priceRub) ? Math.round(priceRub * 100) : null;
|
||||
if (!Number.isFinite(productId) || price_cents == null || price_cents < 0) {
|
||||
return res.redirect('/admin/products?pricing_error=' + encodeURIComponent('Некорректная цена'));
|
||||
}
|
||||
await query(
|
||||
`UPDATE products SET price_cents = $1, sale_price_cents = NULL, sale_ends_at = NULL WHERE id = $2`,
|
||||
[price_cents, productId]
|
||||
);
|
||||
return res.redirect('/admin/products?pricing_updated=1');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(productId) || !Number.isFinite(priceRub) || priceRub < 0) {
|
||||
return res.redirect('/admin/products?pricing_error=' + encodeURIComponent('Некорректная цена'));
|
||||
}
|
||||
|
||||
const { rows: existingRows } = await query(
|
||||
'SELECT sale_price_cents, sale_ends_at FROM products WHERE id = $1',
|
||||
[productId]
|
||||
);
|
||||
const existing = existingRows[0] || {};
|
||||
|
||||
const price_cents = Math.round(priceRub * 100);
|
||||
let sale_price_cents = existing.sale_price_cents ?? null;
|
||||
let sale_ends_at = existing.sale_ends_at ?? null;
|
||||
|
||||
if (saleRubRaw !== '') {
|
||||
const saleRub = parseFloat(saleRubRaw.replace(',', '.'));
|
||||
if (!Number.isFinite(saleRub) || saleRub < 0) {
|
||||
return res.redirect(
|
||||
'/admin/products?pricing_error=' + encodeURIComponent('Некорректная цена со скидкой')
|
||||
);
|
||||
}
|
||||
sale_price_cents = Math.round(saleRub * 100);
|
||||
if (sale_price_cents >= price_cents) {
|
||||
return res.redirect(
|
||||
'/admin/products?pricing_error=' +
|
||||
encodeURIComponent('Цена со скидкой должна быть ниже обычной')
|
||||
);
|
||||
}
|
||||
} else if (!('sale_ends_at' in req.body)) {
|
||||
sale_price_cents = null;
|
||||
sale_ends_at = null;
|
||||
}
|
||||
|
||||
if ('sale_ends_at' in req.body) {
|
||||
sale_ends_at = req.body.sale_ends_at
|
||||
? new Date(req.body.sale_ends_at).toISOString()
|
||||
: null;
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE products SET price_cents = $1, sale_price_cents = $2, sale_ends_at = $3 WHERE id = $4`,
|
||||
[price_cents, sale_price_cents, sale_ends_at, productId]
|
||||
);
|
||||
|
||||
res.redirect('/admin/products?pricing_updated=1');
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/promo-codes/:id/update',
|
||||
asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const description = (req.body.description || '').trim();
|
||||
const discount_type = req.body.discount_type === 'fixed' ? 'fixed' : 'percent';
|
||||
const discount_value = parseInt(req.body.discount_value, 10);
|
||||
const min_order_cents = Math.max(0, parseInt(req.body.min_order_rub, 10) || 0) * 100;
|
||||
const max_uses =
|
||||
req.body.max_uses === '' || req.body.max_uses == null
|
||||
? null
|
||||
: parseInt(req.body.max_uses, 10);
|
||||
|
||||
const { rows: promoRows } = await query('SELECT expires_at FROM promo_codes WHERE id = $1', [
|
||||
id,
|
||||
]);
|
||||
let expires_at = promoRows[0]?.expires_at;
|
||||
if (req.body.valid_days) {
|
||||
const days = Math.max(1, parseInt(req.body.valid_days, 10) || 7);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + days);
|
||||
expires_at = expires.toISOString();
|
||||
}
|
||||
|
||||
const value =
|
||||
discount_type === 'percent'
|
||||
? Math.min(100, discount_value)
|
||||
: discount_value * 100;
|
||||
|
||||
await query(
|
||||
`UPDATE promo_codes SET
|
||||
description = $1, discount_type = $2, discount_value = $3,
|
||||
expires_at = $4, min_order_cents = $5, max_uses = $6
|
||||
WHERE id = $7`,
|
||||
[description, discount_type, value, expires_at, min_order_cents, max_uses, id]
|
||||
);
|
||||
res.redirect('/admin/promo-codes?updated=1');
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/reservations',
|
||||
asyncHandler(async (req, res) => {
|
||||
@@ -182,7 +298,9 @@ router.get(
|
||||
res.render('admin/promo-codes', {
|
||||
title: 'Промокоды',
|
||||
promos,
|
||||
formatPrice,
|
||||
created: req.query.created === '1',
|
||||
updated: req.query.updated === '1',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
+5
-1
@@ -5,6 +5,7 @@ const { requireAuth } = require('../middleware/auth');
|
||||
const { requireCookieConsent } = require('../middleware/cookieConsent');
|
||||
const { asyncHandler } = require('../utils/asyncHandler');
|
||||
const { buildCartPricing } = require('../services/pricing');
|
||||
const productPrice = require('../utils/productPrice');
|
||||
const promoService = require('../services/promo');
|
||||
const loyaltyService = require('../services/loyalty');
|
||||
|
||||
@@ -18,6 +19,9 @@ function enrichLocals(req, res) {
|
||||
|
||||
router.use((req, res, next) => {
|
||||
enrichLocals(req, res);
|
||||
res.locals.isSaleActive = productPrice.isSaleActive;
|
||||
res.locals.effectivePrice = productPrice.getEffectivePriceCents;
|
||||
res.locals.salePercent = productPrice.salePercent;
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -327,7 +331,7 @@ router.post(
|
||||
await client.query(
|
||||
`INSERT INTO order_items (order_id, product_id, quantity, price_cents)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[orderId, item.id, item.quantity, item.price_cents]
|
||||
[orderId, item.id, item.quantity, item.effective_price_cents ?? item.price_cents]
|
||||
);
|
||||
await client.query('UPDATE products SET stock = stock - $1 WHERE id = $2', [
|
||||
item.quantity,
|
||||
|
||||
Reference in New Issue
Block a user