diff --git a/api/index.php b/api/index.php index 43e69b5..6dfa575 100644 --- a/api/index.php +++ b/api/index.php @@ -21,6 +21,28 @@ function isValidLocation(PDO $db, string $location): bool { return isset($cache[$location]); } +function isValidSubcategory(PDO $db, string $category, string $subcategory): bool { + static $cache = []; + if (!isset($cache[$category])) { + $stmt = $db->prepare("SELECT key FROM subcategories WHERE category = ?"); + $stmt->execute([$category]); + $cache[$category] = array_flip($stmt->fetchAll(PDO::FETCH_COLUMN)); + } + return isset($cache[$category][$subcategory]); +} + +function getRequiredSubcategoryCategories(PDO $db): array { + static $cache = null; + if ($cache === null) { + $stmt = $db->prepare("SELECT value FROM app_settings WHERE key = 'subcategory_required_categories'"); + $stmt->execute(); + $row = $stmt->fetch(); + $decoded = $row ? json_decode($row['value'], true) : null; + $cache = is_array($decoded) ? $decoded : ['bevande']; + } + return $cache; +} + const RECIPE_PANTRY_MIN_MATCH_SCORE = 80; const RECENTLY_EXHAUSTED_DAYS = 30; /** How long to suppress auto-re-add after user bought an item (ms, synced with client blocklist). */ @@ -942,6 +964,18 @@ try { case 'locations_update': locationsUpdate($db); break; + case 'subcategories_list': + subcategoriesList($db); + break; + case 'subcategories_add': + subcategoriesAdd($db); + break; + case 'subcategories_remove': + subcategoriesRemove($db); + break; + case 'subcategories_update': + subcategoriesUpdate($db); + break; case 'recipes_list': recipesList($db); break; @@ -2703,19 +2737,18 @@ function saveProduct(PDO $db): void { ? $input['shopping_name'] : computeShoppingName($input['name'], $input['category'] ?? '', $input['brand'] ?? ''); - // Sous-catégorie obligatoire uniquement pour les boissons + // Sous-catégorie : validée et rendue obligatoire dynamiquement selon la config en base $category = $input['category'] ?? ''; $subcategory = trim($input['subcategory'] ?? ''); - $validSubcategories = ['vin', 'biere', 'spiritueux', 'soda', 'jus', 'eau', 'autre']; - if ($category === 'bevande') { - if ($subcategory === '' || !in_array($subcategory, $validSubcategories, true)) { - http_response_code(400); - echo json_encode(['error' => 'subcategory_required', 'message' => 'Sous-catégorie requise pour les boissons']); - return; - } - } else { - $subcategory = null; + if ($subcategory !== '' && !isValidSubcategory($db, $category, $subcategory)) { + $subcategory = ''; // invalide pour cette catégorie -> ignorée plutôt que de planter } + if (in_array($category, getRequiredSubcategoryCategories($db), true) && $subcategory === '') { + http_response_code(400); + echo json_encode(['error' => 'subcategory_required', 'message' => 'Sous-catégorie requise pour cette catégorie']); + return; + } + $subcategory = $subcategory !== '' ? $subcategory : null; $barcode = normalizeProductBarcode($input['barcode'] ?? null); @@ -12082,6 +12115,95 @@ function locationsUpdate(PDO $db): void { echo json_encode(['success' => true]); } +function subcategoriesList(PDO $db): void { + $rows = $db->query("SELECT id, category, key, label, sort_order FROM subcategories ORDER BY category ASC, sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success' => true, 'subcategories' => $rows]); +} + +function subcategoriesAdd(PDO $db): void { + $input = json_decode(file_get_contents('php://input'), true) ?? []; + $category = trim($input['category'] ?? ''); + $label = trim($input['label'] ?? ''); + + if ($category === '' || $label === '') { + echo json_encode(['success' => false, 'error' => 'category and label required']); + return; + } + + $key = mb_strtolower(trim($label)); + $key = preg_replace('/[^a-z0-9]+/u', '_', $key); + $key = trim($key, '_'); + if ($key === '') { + echo json_encode(['success' => false, 'error' => 'invalid label']); + return; + } + + $stmt = $db->prepare("SELECT id FROM subcategories WHERE category = ? AND key = ?"); + $stmt->execute([$category, $key]); + if ($stmt->fetch()) { + echo json_encode(['success' => false, 'error' => 'subcategory already exists for this category']); + return; + } + + $stmt = $db->prepare("SELECT COALESCE(MAX(sort_order), 0) FROM subcategories WHERE category = ?"); + $stmt->execute([$category]); + $maxOrder = (int)$stmt->fetchColumn(); + + $stmt = $db->prepare("INSERT INTO subcategories (category, key, label, sort_order) VALUES (?, ?, ?, ?)"); + $stmt->execute([$category, $key, $label, $maxOrder + 1]); + + echo json_encode(['success' => true, 'id' => (int)$db->lastInsertId(), 'key' => $key]); +} + +function subcategoriesRemove(PDO $db): void { + $input = json_decode(file_get_contents('php://input'), true) ?? []; + $id = (int)($input['id'] ?? 0); + + if ($id <= 0) { + echo json_encode(['success' => false, 'error' => 'id required']); + return; + } + + $stmt = $db->prepare("SELECT category, key FROM subcategories WHERE id = ?"); + $stmt->execute([$id]); + $row = $stmt->fetch(); + if (!$row) { + echo json_encode(['success' => false, 'error' => 'subcategory not found']); + return; + } + + $stmt = $db->prepare("SELECT COUNT(*) FROM products WHERE category = ? AND subcategory = ?"); + $stmt->execute([$row['category'], $row['key']]); + if ((int)$stmt->fetchColumn() > 0) { + echo json_encode(['success' => false, 'error' => 'subcategory still used by products']); + return; + } + + $db->prepare("DELETE FROM subcategories WHERE id = ?")->execute([$id]); + echo json_encode(['success' => true]); +} + +function subcategoriesUpdate(PDO $db): void { + $input = json_decode(file_get_contents('php://input'), true) ?? []; + $id = (int)($input['id'] ?? 0); + $label = trim($input['label'] ?? ''); + + if ($id <= 0 || $label === '') { + echo json_encode(['success' => false, 'error' => 'id and label required']); + return; + } + + $stmt = $db->prepare("SELECT id FROM subcategories WHERE id = ?"); + $stmt->execute([$id]); + if (!$stmt->fetch()) { + echo json_encode(['success' => false, 'error' => 'subcategory not found']); + return; + } + + $db->prepare("UPDATE subcategories SET label = ? WHERE id = ?")->execute([$label, $id]); + echo json_encode(['success' => true]); +} + // ===== SHARED APP DATA FUNCTIONS ===== function appSettingsGet(PDO $db): void {