feat(admin): full CRUD + enable/disable for locations, categories, subcategories

- Migration 011: add is_active INTEGER NOT NULL DEFAULT 1 to locations, categories, subcategories (idempotent)
- Admin routes: locations edit + toggle, categories toggle, subcategories edit + toggle (parallel endpoints on /catalog)
- Bot-side services: filter is_active=1 on locationService/categoryService read methods (getLocationById/getCategoryById left unfiltered for purchase display)
- Views: edit forms, toggle buttons, Active/Disabled badges, disabled row styling in locations.ejs, categories.ejs, catalog tree
- Add Categories nav item in sidebar (folder icon)
- Tests: 14 new tests for migration idempotency + service is_active filtering (23 total pass)
This commit is contained in:
NW
2026-07-19 23:22:21 +01:00
parent 776d0e8552
commit ca2ddefd7a
12 changed files with 512 additions and 27 deletions

View File

@@ -5,7 +5,7 @@ class CategoryService {
static async getCategoriesByLocationId(locationId) {
try {
const categories = await db.allAsync(
'SELECT * FROM categories WHERE location_id = ?',
'SELECT * FROM categories WHERE location_id = ? AND is_active = 1',
[locationId]
);
return categories;
@@ -17,7 +17,7 @@ class CategoryService {
static async getSubcategoriesByCategoryId(categoryId) {
return await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
'SELECT id, name FROM subcategories WHERE category_id = ? AND is_active = 1 ORDER BY name',
[categoryId]
);
}

View File

@@ -3,26 +3,26 @@ import logger from "../utils/logger.js";
class LocationService {
static async getCountries() {
return await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
return await db.allAsync('SELECT DISTINCT country FROM locations WHERE is_active = 1 ORDER BY country');
}
static async getCitiesByCountry(country) {
return await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
'SELECT DISTINCT city FROM locations WHERE country = ? AND is_active = 1 ORDER BY city',
[country]
);
}
static async getDistrictsByCountryAndCity(country, city) {
return await db.allAsync(
'SELECT id, district FROM locations WHERE country = ? AND city = ? ORDER BY district',
'SELECT id, district FROM locations WHERE country = ? AND city = ? AND is_active = 1 ORDER BY district',
[country, city]
);
}
static async getLocationsByCountryAndCity(country, city) {
return await db.allAsync(
'SELECT id, country, city, district FROM locations WHERE country = ? AND city = ? ORDER BY district',
'SELECT id, country, city, district FROM locations WHERE country = ? AND city = ? AND is_active = 1 ORDER BY district',
[country, city]
);
}
@@ -30,7 +30,7 @@ class LocationService {
static async getLocation(country, city, district) {
try {
const location = await db.getAsync(
'SELECT * FROM locations WHERE country = ? AND city = ? AND district = ?',
'SELECT * FROM locations WHERE country = ? AND city = ? AND district = ? AND is_active = 1',
[country, city, district]
);
return location;