Favorieten toegevoegd aan de navigatiebalk en sidebar.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
const FavoriteService = require("../services/FavoriteService");
|
||||
|
||||
class FavoriteController {
|
||||
index(req, res) {
|
||||
const favorites = FavoriteService.getFavorites(req.user.id, req.user.household_id);
|
||||
|
||||
res.locals.title = "Favorieten";
|
||||
res.render("favorites/index", {
|
||||
favorites,
|
||||
user: req.user
|
||||
});
|
||||
}
|
||||
|
||||
toggle(req, res) {
|
||||
const productId = Number.parseInt(req.params.id, 10);
|
||||
|
||||
if (!Number.isInteger(productId)) {
|
||||
req.flash("error", "Product niet gevonden.");
|
||||
return res.redirect("/products");
|
||||
}
|
||||
|
||||
const isFavorite = FavoriteService.toggleFavorite(req.user.id, productId, req.user.household_id);
|
||||
if (!isFavorite && !FavoriteService.getFavoriteIds(req.user.id, req.user.household_id).has(productId)) {
|
||||
req.flash("error", "Product niet gevonden.");
|
||||
return res.redirect("/products");
|
||||
}
|
||||
|
||||
req.flash("success", isFavorite ? "Product toegevoegd aan favorieten." : "Product verwijderd uit favorieten.");
|
||||
res.redirect("/products");
|
||||
}
|
||||
|
||||
remove(req, res) {
|
||||
const productId = Number.parseInt(req.params.productId, 10);
|
||||
|
||||
if (!Number.isInteger(productId)) {
|
||||
req.flash("error", "Product niet gevonden.");
|
||||
return res.redirect("/favorites");
|
||||
}
|
||||
|
||||
const removed = FavoriteService.removeFavorite(req.user.id, productId, req.user.household_id);
|
||||
if (!removed) {
|
||||
req.flash("error", "Product niet gevonden in favorieten.");
|
||||
return res.redirect("/favorites");
|
||||
}
|
||||
|
||||
req.flash("success", "Product verwijderd uit favorieten.");
|
||||
res.redirect("/favorites");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FavoriteController();
|
||||
@@ -2,6 +2,7 @@ const ProductService =
|
||||
require("../services/ProductService");
|
||||
|
||||
const CategoryService = require("../services/CategoryService");
|
||||
const FavoriteService = require("../services/FavoriteService");
|
||||
|
||||
const parseCsv = require("../utils/parseCsv");
|
||||
|
||||
@@ -10,16 +11,18 @@ class ProductController {
|
||||
|
||||
index(req, res) {
|
||||
|
||||
const products =
|
||||
ProductService.getProducts(req.user.household_id);
|
||||
|
||||
const products = ProductService.getProducts(req.user.household_id);
|
||||
const favoriteIds = FavoriteService.getFavoriteIds(req.user.id, req.user.household_id);
|
||||
const categories = CategoryService.getCategories(req.user.household_id);
|
||||
|
||||
res.render(
|
||||
"products/index",
|
||||
{
|
||||
title:"Producten",
|
||||
products,
|
||||
products: products.map(product => ({
|
||||
...product,
|
||||
is_favorite: favoriteIds.has(product.id)
|
||||
})),
|
||||
categories
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const ProductRepository = require("../repositories/ProductRepository");
|
||||
const ShoppingListRepository = require("../repositories/ShoppingListRepository");
|
||||
const FavoriteService = require("../services/FavoriteService");
|
||||
|
||||
class ShoppingListController {
|
||||
index(req, res) {
|
||||
@@ -63,6 +64,40 @@ class ShoppingListController {
|
||||
res.redirect(`/lists/${list.id}`);
|
||||
}
|
||||
|
||||
clearItems(req, res) {
|
||||
const list = ShoppingListRepository.findById(req.params.id, req.user.household_id);
|
||||
if (!list) return res.status(404).render("errors/404");
|
||||
|
||||
ShoppingListRepository.clearItems(list.id);
|
||||
this.broadcastUpdate(req, list.id);
|
||||
req.flash("success", "Alle producten zijn verwijderd uit de lijst.");
|
||||
res.redirect(`/lists/${list.id}`);
|
||||
}
|
||||
|
||||
addFavoriteProducts(req, res) {
|
||||
const list = ShoppingListRepository.findById(req.params.id, req.user.household_id);
|
||||
if (!list) return res.status(404).render("errors/404");
|
||||
|
||||
const favorites = FavoriteService.getFavorites(req.user.id, req.user.household_id);
|
||||
const items = favorites.map((favorite) => ({
|
||||
list_id: list.id,
|
||||
product_id: favorite.id,
|
||||
amount: 1,
|
||||
checked: 0,
|
||||
sort_order: 0
|
||||
}));
|
||||
|
||||
if (items.length === 0) {
|
||||
req.flash("info", "Je hebt nog geen favorieten om toe te voegen.");
|
||||
return res.redirect(`/lists/${list.id}`);
|
||||
}
|
||||
|
||||
ShoppingListRepository.addItems(list.id, items);
|
||||
this.broadcastUpdate(req, list.id);
|
||||
req.flash("success", `${items.length} favoriete producten toegevoegd aan de lijst.`);
|
||||
res.redirect(`/lists/${list.id}`);
|
||||
}
|
||||
|
||||
deleteItem(req, res) {
|
||||
const list = ShoppingListRepository.findById(req.params.id, req.user.household_id);
|
||||
if (!list) return res.status(404).json({ error: "Lijst niet gevonden" });
|
||||
|
||||
+31
-4
@@ -163,18 +163,18 @@ CREATE TABLE IF NOT EXISTS product_favorites (
|
||||
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
user_id INTEGER NOT NULL,
|
||||
household_id INTEGER NOT NULL,
|
||||
|
||||
product_id INTEGER NOT NULL,
|
||||
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
|
||||
UNIQUE(user_id, product_id),
|
||||
UNIQUE(household_id, product_id),
|
||||
|
||||
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users(id)
|
||||
FOREIGN KEY(household_id)
|
||||
REFERENCES households(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
|
||||
@@ -217,8 +217,35 @@ const addColumnIfMissing = (table, column, definition) => {
|
||||
}
|
||||
};
|
||||
|
||||
const migrateFavoriteTableToHouseholdScope = () => {
|
||||
const columns = db.prepare(`PRAGMA table_info(product_favorites)`).all();
|
||||
const hasHouseholdId = columns.some(column => column.name === "household_id");
|
||||
|
||||
if (!hasHouseholdId) {
|
||||
db.exec(`ALTER TABLE product_favorites ADD COLUMN household_id INTEGER`);
|
||||
db.exec(`UPDATE product_favorites
|
||||
SET household_id = (
|
||||
SELECT products.household_id
|
||||
FROM products
|
||||
WHERE products.id = product_favorites.product_id
|
||||
)
|
||||
WHERE household_id IS NULL`);
|
||||
|
||||
db.exec(`DELETE FROM product_favorites
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id)
|
||||
FROM product_favorites
|
||||
GROUP BY household_id, product_id
|
||||
)`);
|
||||
|
||||
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_product_favorites_household_product
|
||||
ON product_favorites (household_id, product_id)`);
|
||||
}
|
||||
};
|
||||
|
||||
addColumnIfMissing("categories", "household_id", "INTEGER REFERENCES households(id)");
|
||||
addColumnIfMissing("products", "household_id", "INTEGER REFERENCES households(id)");
|
||||
migrateFavoriteTableToHouseholdScope();
|
||||
|
||||
const defaultHousehold = db.prepare("SELECT id FROM households ORDER BY id LIMIT 1").get();
|
||||
if (defaultHousehold) {
|
||||
|
||||
+6
-5
@@ -1,17 +1,18 @@
|
||||
module.exports = function(req, res, next) {
|
||||
|
||||
|
||||
if (!req.session.userId) {
|
||||
|
||||
if (!req.session || !req.session.userId || !req.user) {
|
||||
if (req.originalUrl.startsWith("/api/")) {
|
||||
return res.status(401).json({ error: "Je bent niet ingelogd" });
|
||||
}
|
||||
|
||||
if (req.session && req.session.userId) {
|
||||
delete req.session.userId;
|
||||
}
|
||||
|
||||
req.flash("info", "Je sessie is verlopen. Log opnieuw in.");
|
||||
return res.redirect("/login");
|
||||
|
||||
}
|
||||
|
||||
|
||||
next();
|
||||
|
||||
};
|
||||
|
||||
+12
-8
@@ -4,17 +4,21 @@ require("../repositories/UserRepository");
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
|
||||
req.user = null;
|
||||
|
||||
if (req.session.userId) {
|
||||
|
||||
req.user =
|
||||
UserRepository.findById(
|
||||
req.session.userId
|
||||
);
|
||||
|
||||
if (!req.session || !req.session.userId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
req.user = UserRepository.findById(req.session.userId);
|
||||
|
||||
next();
|
||||
if (!req.user) {
|
||||
delete req.session.userId;
|
||||
if (typeof req.session.destroy === "function") {
|
||||
return req.session.destroy(() => next());
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
const db = require("../config/database");
|
||||
|
||||
class FavoriteRepository {
|
||||
getByHousehold(householdId) {
|
||||
return db.prepare(`
|
||||
SELECT products.id,
|
||||
products.name,
|
||||
products.category_id,
|
||||
products.barcode,
|
||||
categories.name AS category_name,
|
||||
categories.icon AS category_icon
|
||||
FROM product_favorites
|
||||
JOIN products ON products.id = product_favorites.product_id
|
||||
LEFT JOIN categories ON categories.id = products.category_id
|
||||
WHERE product_favorites.household_id = ?
|
||||
AND products.household_id = ?
|
||||
ORDER BY products.name ASC
|
||||
`).all(householdId, householdId);
|
||||
}
|
||||
|
||||
getIdsByHousehold(householdId) {
|
||||
return new Set(
|
||||
db.prepare(`
|
||||
SELECT product_favorites.product_id
|
||||
FROM product_favorites
|
||||
JOIN products ON products.id = product_favorites.product_id
|
||||
WHERE product_favorites.household_id = ?
|
||||
AND products.household_id = ?
|
||||
`).all(householdId, householdId).map(row => row.product_id)
|
||||
);
|
||||
}
|
||||
|
||||
find(householdId, productId) {
|
||||
return db.prepare(`
|
||||
SELECT * FROM product_favorites
|
||||
WHERE household_id = ? AND product_id = ?
|
||||
`).get(householdId, productId);
|
||||
}
|
||||
|
||||
add(householdId, productId) {
|
||||
return db.prepare(`
|
||||
INSERT OR IGNORE INTO product_favorites (household_id, product_id)
|
||||
VALUES (?, ?)
|
||||
`).run(householdId, productId);
|
||||
}
|
||||
|
||||
remove(householdId, productId) {
|
||||
return db.prepare(`
|
||||
DELETE FROM product_favorites
|
||||
WHERE household_id = ? AND product_id = ?
|
||||
`).run(householdId, productId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FavoriteRepository();
|
||||
@@ -72,6 +72,37 @@ class ShoppingListRepository {
|
||||
`).run(listId);
|
||||
}
|
||||
|
||||
clearItems(listId) {
|
||||
return db.prepare(`
|
||||
DELETE FROM shopping_list_items
|
||||
WHERE list_id = ?
|
||||
`).run(listId);
|
||||
}
|
||||
|
||||
addItems(listId, items) {
|
||||
if (!items.length) return 0;
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO shopping_list_items (list_id, product_id, amount, checked, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const transaction = db.transaction(() => {
|
||||
for (const item of items) {
|
||||
insert.run(
|
||||
listId,
|
||||
item.product_id,
|
||||
item.amount ?? 1,
|
||||
item.checked ?? 0,
|
||||
item.sort_order ?? 0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
transaction();
|
||||
return items.length;
|
||||
}
|
||||
|
||||
deleteItem(listId, itemId) {
|
||||
return db.prepare(`
|
||||
DELETE FROM shopping_list_items WHERE id = ? AND list_id = ?
|
||||
|
||||
@@ -34,6 +34,8 @@ require("../controllers/CategoryController");
|
||||
const ShoppingListController =
|
||||
require("../controllers/ShoppingListController");
|
||||
|
||||
const FavoriteController = require("../controllers/FavoriteController");
|
||||
|
||||
const MenuController =
|
||||
require("../controllers/MenuController");
|
||||
|
||||
@@ -119,6 +121,24 @@ router.post(
|
||||
ProductController.remove.bind(ProductController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/products/:id/favorite",
|
||||
auth,
|
||||
FavoriteController.toggle.bind(FavoriteController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/favorites",
|
||||
auth,
|
||||
FavoriteController.index.bind(FavoriteController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/favorites/:productId/delete",
|
||||
auth,
|
||||
FavoriteController.remove.bind(FavoriteController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/lists",
|
||||
auth,
|
||||
@@ -149,6 +169,18 @@ router.post(
|
||||
ShoppingListController.create.bind(ShoppingListController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/lists/:id/favorites/add",
|
||||
auth,
|
||||
ShoppingListController.addFavoriteProducts.bind(ShoppingListController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/lists/:id/items/clear",
|
||||
auth,
|
||||
ShoppingListController.clearItems.bind(ShoppingListController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/lists/:id",
|
||||
auth,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const FavoriteRepository = require("../repositories/FavoriteRepository");
|
||||
const ProductRepository = require("../repositories/ProductRepository");
|
||||
|
||||
class FavoriteService {
|
||||
getFavorites(userId, householdId) {
|
||||
return FavoriteRepository.getByHousehold(householdId);
|
||||
}
|
||||
|
||||
getFavoriteIds(userId, householdId) {
|
||||
return FavoriteRepository.getIdsByHousehold(householdId);
|
||||
}
|
||||
|
||||
toggleFavorite(userId, productId, householdId) {
|
||||
const product = ProductRepository.findById(productId, householdId);
|
||||
if (!product) return false;
|
||||
|
||||
const current = FavoriteRepository.find(householdId, productId);
|
||||
if (current) {
|
||||
FavoriteRepository.remove(householdId, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
FavoriteRepository.add(householdId, productId);
|
||||
return true;
|
||||
}
|
||||
|
||||
removeFavorite(userId, productId, householdId) {
|
||||
const product = ProductRepository.findById(productId, householdId);
|
||||
if (!product) return false;
|
||||
|
||||
return FavoriteRepository.remove(householdId, productId).changes > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FavoriteService();
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
<div class="page-header mb-4">
|
||||
<h1>💙 Favorieten</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<% if (favorites.length === 0) { %>
|
||||
<p class="text-secondary">Je hebt nog geen favoriete producten.</p>
|
||||
<% } else { %>
|
||||
<div class="list-group">
|
||||
<% favorites.forEach(product => { %>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<strong><%= product.category_icon || "" %> <%= product.name %></strong>
|
||||
<div class="text-secondary"><%= product.category_name || "Geen categorie" %></div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/favorites/<%= product.id %>/delete">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Verwijder</button>
|
||||
</form>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
+14
-4
@@ -2,10 +2,20 @@
|
||||
<a href="/lists" class="btn btn-outline-secondary mb-3">← Terug naar lijsten</a>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||
<h1 class="mb-0"><%= list.name %></h1>
|
||||
<form method="post" action="/lists/<%= list.id %>/items/reset"
|
||||
onsubmit="return confirm('Alle producten weer openzetten?');">
|
||||
<button class="btn btn-outline-primary" type="submit">Alles weer openzetten</button>
|
||||
</form>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="post" action="/lists/<%= list.id %>/favorites/add"
|
||||
onsubmit="return confirm('Alle favorieten toevoegen aan deze lijst?');">
|
||||
<button class="btn btn-outline-primary" type="submit">Alle favorieten toevoegen</button>
|
||||
</form>
|
||||
<form method="post" action="/lists/<%= list.id %>/items/reset"
|
||||
onsubmit="return confirm('Alle producten weer openzetten?');">
|
||||
<button class="btn btn-outline-primary" type="submit">Alles weer openzetten</button>
|
||||
</form>
|
||||
<form method="post" action="/lists/<%= list.id %>/items/clear"
|
||||
onsubmit="return confirm('Weet je zeker dat je alle producten uit deze lijst wilt verwijderen?');">
|
||||
<button class="btn btn-outline-danger" type="submit">Alles verwijderen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<a class="nav-link" href="/">Dashboard</a>
|
||||
<a class="nav-link" href="/lists">Lijsten</a>
|
||||
<a class="nav-link" href="/products">Producten</a>
|
||||
<a class="nav-link" href="/favorites">Favorieten</a>
|
||||
<a class="nav-link" href="/categories">Categorieën</a>
|
||||
<a class="nav-link" href="/household">Huishouden</a>
|
||||
<a class="nav-link text-danger" href="/logout">Uitloggen</a>
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
|
||||
</a>
|
||||
|
||||
<a class="nav-link" href="/favorites">
|
||||
|
||||
💙 Favorieten
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
<a class="nav-link" href="/household">
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<button class="btn btn-primary" type="submit" form="product-edit-<%= product.id %>">Wijzigingen opslaan</button>
|
||||
<form method="post" action="/products/<%= product.id %>/delete"
|
||||
onsubmit="return confirm('Weet je zeker dat je dit product wilt verwijderen?');">
|
||||
<button class="btn btn-outline-danger" type="submit">Product verwijderen</button>
|
||||
<button class="btn btn-light border text-danger" type="submit">Product verwijderen</button>
|
||||
</form>
|
||||
<a class="btn btn-link" href="/products">Annuleren</a>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,18 @@
|
||||
<strong><%= product.category_icon || "" %> <%= product.name %></strong>
|
||||
<div class="text-secondary"><%= product.category_name || "Geen categorie" %></div>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/products/<%= product.id %>/edit">
|
||||
Wijzigen
|
||||
</a>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<form method="post" action="/products/<%= product.id %>/favorite">
|
||||
<button class="btn btn-sm <%= product.is_favorite ? 'btn-danger' : 'btn-outline-secondary' %>"
|
||||
type="submit"
|
||||
aria-label="<%= product.is_favorite ? 'Verwijder uit favorieten' : 'Toevoegen aan favorieten' %>">
|
||||
<%= product.is_favorite ? '♥' : '♡' %>
|
||||
</button>
|
||||
</form>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/products/<%= product.id %>/edit">
|
||||
Wijzigen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
|
||||
Reference in New Issue
Block a user