From c0c1a312c6cc9ad80424d4b296ce11e43f95dc8f Mon Sep 17 00:00:00 2001 From: morgane Date: Thu, 18 Jun 2026 08:43:08 +0000 Subject: [PATCH] Actualiser assets/js/app.js --- assets/js/app.js | 238 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 223 insertions(+), 15 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 3352a2d..e8f0a53 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1384,6 +1384,140 @@ async function _loadConfigPage() { } catch (e) { container.innerHTML = `

Erreur de chargement.

`; } + await _loadSubcategoryConfigSection(); +} + +async function _loadSubcategoryConfigSection() { + const catSelect = document.getElementById('subcat-config-category'); + if (!catSelect) return; + if (!catSelect.dataset.populated) { + catSelect.innerHTML = Object.entries(CATEGORY_LABELS).map(([key, label]) => ``).join(''); + catSelect.dataset.populated = 'true'; + } + await renderSubcategoryConfigForCategory(catSelect.value || Object.keys(CATEGORY_LABELS)[0]); +} + +async function renderSubcategoryConfigForCategory(category) { + const catSelect = document.getElementById('subcat-config-category'); + if (catSelect) catSelect.value = category; + const requiredCheckbox = document.getElementById('subcat-config-required'); + if (requiredCheckbox) requiredCheckbox.checked = REQUIRED_SUBCATEGORY_CATEGORIES.includes(category); + + const container = document.getElementById('subcat-list-container'); + if (!container) return; + const list = SUBCATEGORIES_BY_CATEGORY[category] || []; + if (list.length === 0) { + container.innerHTML = `

Aucune sous-catégorie pour cette catégorie.

`; + return; + } + container.innerHTML = list.map(sc => ` +
+ + + +
+ `).join(''); +} + +async function onSubcatConfigCategoryChange() { + const cat = document.getElementById('subcat-config-category')?.value || ''; + await renderSubcategoryConfigForCategory(cat); +} + +async function toggleSubcategoryRequired() { + const category = document.getElementById('subcat-config-category')?.value || ''; + const checked = document.getElementById('subcat-config-required')?.checked; + let updated = REQUIRED_SUBCATEGORY_CATEGORIES.slice(); + if (checked && !updated.includes(category)) updated.push(category); + if (!checked) updated = updated.filter(c => c !== category); + + showLoading(true); + try { + const result = await api('app_settings_save', {}, 'POST', { settings: { subcategory_required_categories: updated } }); + showLoading(false); + if (result.success) { + REQUIRED_SUBCATEGORY_CATEGORIES = updated; + showToast('Préférence enregistrée', 'success'); + } else { + showToast('Erreur lors de l\'enregistrement', 'error'); + } + } catch (e) { + showLoading(false); + showToast('Erreur lors de l\'enregistrement', 'error'); + } +} + +async function addSubcategoryRow() { + const category = document.getElementById('subcat-config-category')?.value || ''; + const labelInput = document.getElementById('new-subcat-label'); + const label = labelInput.value.trim(); + if (!category) { showToast('Choisis une catégorie', 'warning'); return; } + if (!label) { showToast('Indique un nom pour la nouvelle sous-catégorie', 'warning'); return; } + + showLoading(true); + try { + const result = await api('subcategories_add', {}, 'POST', { category, label }); + showLoading(false); + if (result.success) { + showToast(`Sous-catégorie "${label}" ajoutée`, 'success'); + labelInput.value = ''; + if (!SUBCATEGORIES_BY_CATEGORY[category]) SUBCATEGORIES_BY_CATEGORY[category] = []; + SUBCATEGORIES_BY_CATEGORY[category].push({ id: result.id, category, key: result.key, label }); + SUBCATEGORY_LABEL_LOOKUP[category + '::' + result.key] = label; + renderSubcategoryConfigForCategory(category); + } else { + showToast(result.error || 'Erreur lors de l\'ajout', 'error'); + } + } catch (e) { + showLoading(false); + showToast('Erreur lors de l\'ajout', 'error'); + } +} + +async function updateSubcategoryRow(id) { + const label = document.getElementById(`subcat-label-${id}`)?.value.trim(); + if (!label) { showToast('Le nom ne peut pas être vide', 'warning'); return; } + + showLoading(true); + try { + const result = await api('subcategories_update', {}, 'POST', { id, label }); + showLoading(false); + if (result.success) { + showToast('Sous-catégorie mise à jour', 'success'); + for (const cat in SUBCATEGORIES_BY_CATEGORY) { + const row = SUBCATEGORIES_BY_CATEGORY[cat].find(sc => sc.id === id); + if (row) { row.label = label; SUBCATEGORY_LABEL_LOOKUP[cat + '::' + row.key] = label; break; } + } + } else { + showToast(result.error || 'Erreur', 'error'); + } + } catch (e) { + showLoading(false); + showToast('Erreur', 'error'); + } +} + +async function removeSubcategoryRow(id) { + if (!confirm('Supprimer cette sous-catégorie ?')) return; + + showLoading(true); + try { + const result = await api('subcategories_remove', {}, 'POST', { id }); + showLoading(false); + if (result.success) { + showToast('Sous-catégorie supprimée', 'success'); + for (const cat in SUBCATEGORIES_BY_CATEGORY) { + SUBCATEGORIES_BY_CATEGORY[cat] = SUBCATEGORIES_BY_CATEGORY[cat].filter(sc => sc.id !== id); + } + const category = document.getElementById('subcat-config-category')?.value || ''; + renderSubcategoryConfigForCategory(category); + } else { + showToast(result.error || 'Impossible de supprimer', 'error'); + } + } catch (e) { + showLoading(false); + showToast('Erreur lors de la suppression', 'error'); + } } function renderLocationsList(locations) { @@ -1817,10 +1951,57 @@ const CATEGORY_LABELS = { 'altro': `📦 ${t('categories.altro')}` }; -const SUBCATEGORY_LABELS = { - 'vin': '🍷 Vin', 'biere': '🍺 Bière', 'spiritueux': '🥃 Spiritueux', - 'soda': '🥤 Soda', 'jus': '🧃 Jus', 'eau': '💧 Eau', 'autre': '📦 Autre' -}; +// Sous-catégories chargées dynamiquement depuis la base (table subcategories), gérables via Config +let SUBCATEGORIES_BY_CATEGORY = {}; // { category: [{id, category, key, label}, ...] } +let SUBCATEGORY_LABEL_LOOKUP = {}; // { "category::key": label } — évite les collisions entre catégories +let REQUIRED_SUBCATEGORY_CATEGORIES = ['bevande']; // valeur par défaut, écrasée par syncSettingsFromDB() + +async function loadCustomSubcategories() { + try { + const result = await api('subcategories_list', {}, 'GET'); + if (result.success && Array.isArray(result.subcategories)) { + SUBCATEGORIES_BY_CATEGORY = {}; + SUBCATEGORY_LABEL_LOOKUP = {}; + result.subcategories.forEach(sc => { + if (!SUBCATEGORIES_BY_CATEGORY[sc.category]) SUBCATEGORIES_BY_CATEGORY[sc.category] = []; + SUBCATEGORIES_BY_CATEGORY[sc.category].push(sc); + SUBCATEGORY_LABEL_LOOKUP[sc.category + '::' + sc.key] = sc.label; + }); + } + } catch (e) { + console.warn('[EverShelf] Could not load subcategories:', e); + } +} + +function getSubcategoryOptionsHtml(category, selectedValue = '') { + const list = SUBCATEGORIES_BY_CATEGORY[category]; + if (!list || list.length === 0) return ''; + const opts = list.map(sc => + `` + ).join(''); + return '' + opts; +} + +function updateSubcategoryField(selectId, groupId, category, selectedValue = '') { + const select = document.getElementById(selectId); + const group = document.getElementById(groupId); + if (!select) return; + const list = SUBCATEGORIES_BY_CATEGORY[category]; + select.innerHTML = getSubcategoryOptionsHtml(category, selectedValue); + if (group) { + group.style.display = (list && list.length > 0) ? 'block' : 'none'; + const mark = group.querySelector('.subcategory-required-mark'); + if (mark) { + mark.style.color = '#e74c3c'; + mark.style.display = REQUIRED_SUBCATEGORY_CATEGORIES.includes(category) ? 'inline' : 'none'; + } + } +} + +function onEditActionCategoryChange() { + const cat = document.getElementById('edit-action-category')?.value || ''; + updateSubcategoryField('edit-action-subcategory', 'edit-action-subcategory-group', cat); +} // Detect best unit/quantity from Open Food Facts quantity_info string // Returns the actual package weight/volume as default (e.g. 700g → unit:'g', quantity:700) @@ -2789,6 +2970,7 @@ async function syncSettingsFromDB() { const srv = res.settings; if (srv.review_confirmed) _reviewConfirmedCache = srv.review_confirmed; + if (Array.isArray(srv.subcategory_required_categories)) REQUIRED_SUBCATEGORY_CATEGORIES = srv.subcategory_required_categories; // meal_plan is stored in SQLite app_settings so all devices stay in sync if (srv.meal_plan) { @@ -6676,8 +6858,8 @@ function renderInventoryItem(item) { const catIcon = CATEGORY_ICONS[catKey] || '📦'; const catLabel = t('categories.' + catKey) || catKey; const catBadge = `${catIcon} ${catLabel}`; - const subBadge = (item.subcategory && SUBCATEGORY_LABELS[item.subcategory]) - ? `${SUBCATEGORY_LABELS[item.subcategory]}` + const subBadge = (item.subcategory && SUBCATEGORY_LABEL_LOOKUP[catKey + '::' + item.subcategory]) + ? `${SUBCATEGORY_LABEL_LOOKUP[catKey + '::' + item.subcategory]}` : ''; const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location }; const days = daysUntilExpiry(item.expiry_date); @@ -6786,7 +6968,7 @@ function filterInventory() { // Match by inferred category: "biscotti" → queryCat="snack" → all snack items if (queryCat !== 'altro' && itemCat === queryCat) return true; if (i.subcategory && i.subcategory.toLowerCase().includes(q)) return true; - if (i.subcategory && (SUBCATEGORY_LABELS[i.subcategory] || '').toLowerCase().includes(q)) return true; + if (i.subcategory && (SUBCATEGORY_LABEL_LOOKUP[itemCat + '::' + i.subcategory] || '').toLowerCase().includes(q)) return true; return false; }); renderInventory(filtered); @@ -8397,6 +8579,7 @@ function startManualEntry(barcode = '') { document.getElementById('pf-name').value = ''; document.getElementById('pf-brand').value = ''; document.getElementById('pf-category').value = ''; + updateSubcategoryField('pf-subcategory', 'pf-subcategory-group', ''); document.getElementById('pf-unit').value = 'pz'; document.getElementById('pf-defqty').value = '1'; document.getElementById('pf-notes').value = ''; @@ -8508,7 +8691,10 @@ function onCategoryChange(fromAutoDetect = false) { const cat = document.getElementById('pf-category').value; const unitSelect = document.getElementById('pf-unit'); const qtyInput = document.getElementById('pf-defqty'); - + + // Met à jour les options/visibilité de la sous-catégorie pour la nouvelle catégorie + updateSubcategoryField('pf-subcategory', 'pf-subcategory-group', cat); + // If user manually changed category via dropdown, don't auto-fill qty/unit if (!fromAutoDetect) { // Mark qty as "set" so future auto-detects won't overwrite either @@ -8668,8 +8854,8 @@ async function submitProduct(e) { e.preventDefault(); const pfCategory = document.getElementById('pf-category').value; const pfSubcategory = document.getElementById('pf-subcategory')?.value || ''; - if (pfCategory === 'bevande' && !pfSubcategory) { - showToast('Merci de préciser le type de boisson', 'error'); + if (REQUIRED_SUBCATEGORY_CATEGORIES.includes(pfCategory) && !pfSubcategory) { + showToast('Merci de préciser la sous-catégorie', 'error'); document.getElementById('pf-subcategory').focus(); return; } @@ -8805,9 +8991,14 @@ function showProductAction() { } // Always build the edit form, but only show it auto-opened for unknown products - const categoryOptions = Object.entries(CATEGORY_LABELS).map(([key, label]) => - `` + const currentCatForEdit = mapToLocalCategory(currentProduct.category, currentProduct.name, currentProduct.brand); + const categoryOptions = Object.entries(CATEGORY_LABELS).map(([key, label]) => + `` ).join(''); + const editSubcategoryOptions = getSubcategoryOptionsHtml(currentCatForEdit, currentProduct.subcategory || ''); + const editSubcategoryList = SUBCATEGORIES_BY_CATEGORY[currentCatForEdit]; + const editSubcategoryVisible = (editSubcategoryList && editSubcategoryList.length > 0) ? 'block' : 'none'; + const editSubcategoryRequired = REQUIRED_SUBCATEGORY_CATEGORIES.includes(currentCatForEdit); editInfoEl.innerHTML = `
@@ -8824,11 +9015,17 @@ function showProductAction() {
- ${categoryOptions}
+
+ + +
@@ -9399,8 +9596,14 @@ async function saveEditedProductInfo() { } const brand = (document.getElementById('edit-action-brand')?.value || '').trim(); const category = document.getElementById('edit-action-category')?.value || ''; + const subcategory = document.getElementById('edit-action-subcategory')?.value || ''; + const effectiveCategory = category || currentProduct.category || ''; + if (REQUIRED_SUBCATEGORY_CATEGORIES.includes(effectiveCategory) && !subcategory) { + showToast('Merci de préciser la sous-catégorie', 'error'); + document.getElementById('edit-action-subcategory')?.focus(); + return; + } const notes = (document.getElementById('edit-action-notes')?.value || '').trim(); - showLoading(true); try { const result = await api('product_save', {}, 'POST', { @@ -9408,7 +9611,8 @@ async function saveEditedProductInfo() { barcode: currentProduct.barcode || null, name: name, brand: brand, - category: category || currentProduct.category || '', + category: effectiveCategory, + subcategory: subcategory || null, image_url: currentProduct.image_url || '', unit: currentProduct.unit || 'pz', default_quantity: currentProduct.default_quantity || 1, @@ -9421,6 +9625,7 @@ async function saveEditedProductInfo() { currentProduct.brand = brand; currentProduct.notes = notes; if (category) currentProduct.category = category; + currentProduct.subcategory = subcategory || null; showToast(t('toast.product_updated'), 'success'); // Refresh the action page with updated data showProductAction(); @@ -19653,6 +19858,9 @@ async function _initApp() { // Load custom locations (merges into LOCATIONS before any UI renders) await loadCustomLocations(); + + // Load custom subcategories (merges into SUBCATEGORIES_BY_CATEGORY before any UI renders) + await loadCustomSubcategories(); // Check for setup wizard resume (after language change) const resumeStep = localStorage.getItem('evershelf_setup_step');