feat: подписка на уведомление о поступлении товара
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+32
-1
@@ -3,6 +3,7 @@ const { query, formatPrice } = require('../db');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
const { asyncHandler } = require('../utils/asyncHandler');
|
||||
const { ROLE_LABELS } = require('../constants/roles');
|
||||
const { notifyIfBackInStock } = require('../services/stock-alerts');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -91,7 +92,9 @@ router.get(
|
||||
'/products',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { rows: products } = await query(
|
||||
`SELECT p.*, c.name AS category_name
|
||||
`SELECT p.*, c.name AS category_name,
|
||||
(SELECT COUNT(*)::int FROM product_stock_alerts a
|
||||
WHERE a.product_id = p.id AND a.notified_at IS NULL) AS alert_count
|
||||
FROM products p
|
||||
LEFT JOIN categories c ON c.id = p.category_id
|
||||
ORDER BY p.id`
|
||||
@@ -100,10 +103,38 @@ router.get(
|
||||
title: 'Товары',
|
||||
products,
|
||||
formatPrice,
|
||||
stockUpdated: req.query.stock_updated === '1',
|
||||
notified: req.query.notified ? parseInt(req.query.notified, 10) : 0,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/products/:id/stock',
|
||||
asyncHandler(async (req, res) => {
|
||||
const productId = parseInt(req.params.id, 10);
|
||||
const stock = parseInt(req.body.stock, 10);
|
||||
if (!Number.isFinite(productId) || !Number.isFinite(stock) || stock < 0) {
|
||||
return res.redirect('/admin/products');
|
||||
}
|
||||
|
||||
const { rows } = await query('SELECT stock FROM products WHERE id = $1', [productId]);
|
||||
const oldStock = rows[0]?.stock ?? 0;
|
||||
|
||||
await query('UPDATE products SET stock = $1 WHERE id = $2', [stock, productId]);
|
||||
|
||||
let notified = 0;
|
||||
if (oldStock <= 0 && stock > 0) {
|
||||
const result = await notifyIfBackInStock(productId);
|
||||
notified = result.sent;
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams({ stock_updated: '1' });
|
||||
if (notified > 0) qs.set('notified', String(notified));
|
||||
res.redirect(`/admin/products?${qs}`);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/reservations',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -89,6 +89,25 @@ router.get(
|
||||
|
||||
const errorMsg = req.query.error ? decodeURIComponent(String(req.query.error)) : null;
|
||||
const reserved = req.query.reserved === '1';
|
||||
const notifySuccess = req.query.notify_success
|
||||
? decodeURIComponent(String(req.query.notify_success))
|
||||
: null;
|
||||
const notifyError = req.query.notify_error
|
||||
? decodeURIComponent(String(req.query.notify_error))
|
||||
: null;
|
||||
|
||||
let stockAlertSubscribed = false;
|
||||
let notifyEmail = res.locals.user?.email || '';
|
||||
if (product.stock <= 0) {
|
||||
const stockAlerts = require('../services/stock-alerts');
|
||||
if (notifyEmail) {
|
||||
stockAlertSubscribed = await stockAlerts.isSubscribed(
|
||||
product.id,
|
||||
notifyEmail,
|
||||
req.session.userId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.render('product', {
|
||||
title: product.name,
|
||||
@@ -96,6 +115,10 @@ router.get(
|
||||
userReservation,
|
||||
error: errorMsg,
|
||||
reserved,
|
||||
notifySuccess,
|
||||
notifyError,
|
||||
stockAlertSubscribed,
|
||||
notifyEmail,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
const express = require('express');
|
||||
const { query, formatPrice } = require('../db');
|
||||
const { getCart, cartCount } = require('../cart');
|
||||
const { requireCookieConsent } = require('../middleware/cookieConsent');
|
||||
const { asyncHandler } = require('../utils/asyncHandler');
|
||||
const stockAlerts = require('../services/stock-alerts');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use((req, res, next) => {
|
||||
const cart = getCart(req);
|
||||
res.locals.cartCount = cartCount(cart);
|
||||
res.locals.formatPrice = formatPrice;
|
||||
next();
|
||||
});
|
||||
|
||||
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
router.post(
|
||||
'/product/:slug/notify-stock',
|
||||
requireCookieConsent,
|
||||
asyncHandler(async (req, res) => {
|
||||
const slug = req.params.slug;
|
||||
const { rows } = await query('SELECT id, name, stock FROM products WHERE slug = $1', [
|
||||
slug,
|
||||
]);
|
||||
const product = rows[0];
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).render('error', {
|
||||
title: 'Не найдено',
|
||||
message: 'Товар не найден',
|
||||
code: 404,
|
||||
});
|
||||
}
|
||||
|
||||
if (product.stock > 0) {
|
||||
return res.redirect(
|
||||
`/product/${slug}?notify_error=${encodeURIComponent('Товар уже в наличии')}`
|
||||
);
|
||||
}
|
||||
|
||||
let email = (req.body.email || '').trim().toLowerCase();
|
||||
if (req.session.userId) {
|
||||
const { rows: users } = await query('SELECT email FROM users WHERE id = $1', [
|
||||
req.session.userId,
|
||||
]);
|
||||
if (users[0]) email = users[0].email;
|
||||
}
|
||||
|
||||
if (!email || !emailRe.test(email)) {
|
||||
return res.redirect(
|
||||
`/product/${slug}?notify_error=${encodeURIComponent('Укажите корректный email')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (await stockAlerts.isSubscribed(product.id, email, req.session.userId)) {
|
||||
return res.redirect(
|
||||
`/product/${slug}?notify_success=${encodeURIComponent('Вы уже подписаны — сообщим на почту')}`
|
||||
);
|
||||
}
|
||||
|
||||
await stockAlerts.subscribe({
|
||||
productId: product.id,
|
||||
email,
|
||||
userId: req.session.userId || null,
|
||||
});
|
||||
|
||||
res.redirect(
|
||||
`/product/${slug}?notify_success=${encodeURIComponent('Когда товар появится, отправим письмо на ' + email)}`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user