Favorieten toegevoegd aan de navigatiebalk en sidebar.

This commit is contained in:
2026-08-29 19:53:28 +02:00
parent 2912f7c455
commit ce07575d5e
19 changed files with 355 additions and 29 deletions
+51
View File
@@ -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();
+7 -4
View File
@@ -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
}
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
};
+55
View File
@@ -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();
+31
View File
@@ -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 = ?
+32
View File
@@ -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,
+35
View File
@@ -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.
+26
View File
@@ -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
View File
@@ -2,10 +2,20 @@
<a href="/lists" class="btn btn-outline-secondary mb-3">&larr; 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>
+1
View File
@@ -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>
+6
View File
@@ -26,6 +26,12 @@
</a>
<a class="nav-link" href="/favorites">
💙 Favorieten
</a>
<a class="nav-link" href="/household">
+1 -1
View File
@@ -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>
+12 -3
View File
@@ -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>
<% }); %>