Actualiser assets/js/app.js
CI / PHP Syntax Check (push) Has been cancelled
CI / JavaScript Lint (push) Has been cancelled
CI / Docker Build Test (push) Has been cancelled
CI / Validate Translation Files (push) Has been cancelled
CI / Auto-merge develop → main (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled

This commit is contained in:
2026-06-18 08:58:15 +00:00
parent 1e40da7235
commit b28c6591a9
+172
View File
@@ -1370,6 +1370,17 @@ function renderLocationButtons(containerId, activeKey, onClickFnName) {
}).join(''); }).join('');
} }
function showConfigTab(tab) {
document.querySelectorAll('.config-tab-content').forEach(el => el.style.display = 'none');
const target = document.getElementById('config-tab-' + tab);
if (target) target.style.display = 'block';
document.querySelectorAll('.config-tab-btn').forEach(btn => {
const isActive = btn.dataset.tab === tab;
btn.classList.toggle('btn-primary', isActive);
btn.classList.toggle('btn-secondary', !isActive);
});
}
async function _loadConfigPage() { async function _loadConfigPage() {
const container = document.getElementById('locations-list-container'); const container = document.getElementById('locations-list-container');
if (!container) return; if (!container) return;
@@ -1384,9 +1395,141 @@ async function _loadConfigPage() {
} catch (e) { } catch (e) {
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`; container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
} }
await _loadCategoriesConfigSection();
await _loadSubcategoryConfigSection(); await _loadSubcategoryConfigSection();
} }
async function _loadCategoriesConfigSection() {
const container = document.getElementById('categories-list-container');
if (!container) return;
container.innerHTML = `<p class="settings-hint">Chargement…</p>`;
try {
const result = await api('categories_list', {}, 'GET');
if (!result.success) {
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
return;
}
renderCategoriesList(result.categories);
} catch (e) {
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
}
}
function renderCategoriesList(categories) {
const container = document.getElementById('categories-list-container');
if (!container) return;
if (!categories || categories.length === 0) {
container.innerHTML = `<p class="settings-hint">Aucune catégorie.</p>`;
return;
}
container.innerHTML = categories.map(cat => {
const builtinBadge = cat.is_builtin
? `<span style="font-size:0.72rem;color:var(--text-light);margin-left:6px">native</span>`
: '';
const deleteBtn = cat.is_builtin
? ''
: `<button class="btn btn-small btn-secondary" onclick="removeCategory('${cat.key}')" title="Supprimer">🗑️</button>`;
return `
<div class="loc-row" style="display:flex;flex-direction:column;gap:6px;padding:8px 0;border-bottom:1px solid var(--border,#e2e8f0)">
<div style="display:flex;align-items:center;gap:8px">
<input type="text" class="form-input" style="max-width:60px;text-align:center" value="${escapeHtml(cat.icon)}" id="cat-icon-${cat.key}" maxlength="4">
<input type="text" class="form-input" style="flex:1" value="${escapeHtml(cat.label)}" id="cat-label-${cat.key}">
${builtinBadge}
<button class="btn btn-small btn-primary" onclick="updateCategory('${cat.key}')" title="Enregistrer">💾</button>
${deleteBtn}
</div>
<input type="text" class="form-input" value="${escapeHtml(cat.keywords || '')}" id="cat-keywords-${cat.key}" placeholder="Mots-clés séparés par des virgules">
</div>
`;
}).join('');
}
async function addCategory() {
const iconInput = document.getElementById('new-category-icon');
const labelInput = document.getElementById('new-category-label');
const keywordsInput = document.getElementById('new-category-keywords');
const icon = iconInput.value.trim() || '📦';
const label = labelInput.value.trim();
const keywords = keywordsInput.value.trim();
if (!label) {
showToast('Indique un nom pour la nouvelle catégorie', 'warning');
return;
}
showLoading(true);
try {
const result = await api('categories_add', {}, 'POST', { label, icon, keywords });
showLoading(false);
if (result.success) {
showToast(`Catégorie "${label}" ajoutée`, 'success');
iconInput.value = '';
labelInput.value = '';
keywordsInput.value = '';
CATEGORY_LABELS[result.key] = label;
CATEGORY_ICONS[result.key] = icon;
if (!CATEGORY_LOCATION[result.key]) CATEGORY_LOCATION[result.key] = 'dispensa';
if (keywords) CUSTOM_CATEGORY_KEYWORDS[result.key] = keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean);
_loadCategoriesConfigSection();
} else {
showToast(result.error || 'Erreur lors de l\'ajout', 'error');
}
} catch (e) {
showLoading(false);
showToast('Erreur lors de l\'ajout', 'error');
}
}
async function updateCategory(key) {
const icon = document.getElementById(`cat-icon-${key}`).value.trim() || '📦';
const label = document.getElementById(`cat-label-${key}`).value.trim();
const keywords = document.getElementById(`cat-keywords-${key}`).value.trim();
if (!label) {
showToast('Le nom ne peut pas être vide', 'warning');
return;
}
showLoading(true);
try {
const result = await api('categories_update', {}, 'POST', { key, label, icon, keywords });
showLoading(false);
if (result.success) {
showToast('Catégorie mise à jour', 'success');
CATEGORY_LABELS[key] = label;
CATEGORY_ICONS[key] = icon;
CUSTOM_CATEGORY_KEYWORDS[key] = keywords ? keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
} else {
showToast(result.error || 'Erreur', 'error');
}
} catch (e) {
showLoading(false);
showToast('Erreur', 'error');
}
}
async function removeCategory(key) {
if (!confirm(`Supprimer la catégorie "${CATEGORY_LABELS[key] || key}" ?`)) return;
showLoading(true);
try {
const result = await api('categories_remove', {}, 'POST', { key });
showLoading(false);
if (result.success) {
showToast('Catégorie supprimée', 'success');
delete CATEGORY_LABELS[key];
delete CATEGORY_ICONS[key];
delete CUSTOM_CATEGORY_KEYWORDS[key];
_loadCategoriesConfigSection();
} else {
showToast(result.error || 'Impossible de supprimer', 'error');
}
} catch (e) {
showLoading(false);
showToast('Erreur lors de la suppression', 'error');
}
}
async function _loadSubcategoryConfigSection() { async function _loadSubcategoryConfigSection() {
const catSelect = document.getElementById('subcat-config-category'); const catSelect = document.getElementById('subcat-config-category');
if (!catSelect) return; if (!catSelect) return;
@@ -1628,6 +1771,28 @@ const CATEGORY_ICONS = {
'cereali': '🌾', 'igiene': '🧴', 'pulizia': '🧹', 'altro': '📦' 'cereali': '🌾', 'igiene': '🧴', 'pulizia': '🧹', 'altro': '📦'
}; };
// Mots-clés des catégories personnalisées (utilisateur), pour la détection auto par nom de produit
let CUSTOM_CATEGORY_KEYWORDS = {};
async function loadCustomCategories() {
try {
const result = await api('categories_list', {}, 'GET');
if (result.success && Array.isArray(result.categories)) {
CUSTOM_CATEGORY_KEYWORDS = {};
result.categories.forEach(cat => {
CATEGORY_LABELS[cat.key] = cat.label;
CATEGORY_ICONS[cat.key] = cat.icon || '📦';
if (!CATEGORY_LOCATION[cat.key]) CATEGORY_LOCATION[cat.key] = 'dispensa';
if (cat.keywords) {
CUSTOM_CATEGORY_KEYWORDS[cat.key] = cat.keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean);
}
});
}
} catch (e) {
console.warn('[EverShelf] Could not load custom categories:', e);
}
}
// Auto-detect location based on category and product name // Auto-detect location based on category and product name
const CATEGORY_LOCATION = { const CATEGORY_LOCATION = {
'latticini': 'frigo', 'carne': 'frigo', 'pesce': 'frigo', 'latticini': 'frigo', 'carne': 'frigo', 'pesce': 'frigo',
@@ -1774,6 +1939,10 @@ function guessCategoryFromName(name, brand = '') {
if (/sapone|shampoo|dentifricio|deodorante|carta igienica|fazzoletti|cotton fioc|assorbente|rasoio|schiuma da barba|gel doccia|balsamo\b|lozione/.test(n)) return 'igiene'; if (/sapone|shampoo|dentifricio|deodorante|carta igienica|fazzoletti|cotton fioc|assorbente|rasoio|schiuma da barba|gel doccia|balsamo\b|lozione/.test(n)) return 'igiene';
// Pulizia casa // Pulizia casa
if (/detersivo|pulito|sgrassatore|candeggina|ammorbidente|anticalcare|bucato|piatti\b|lavatrice|lavastoviglie|detergente/.test(n)) return 'pulizia'; if (/detersivo|pulito|sgrassatore|candeggina|ammorbidente|anticalcare|bucato|piatti\b|lavatrice|lavastoviglie|detergente/.test(n)) return 'pulizia';
// Mots-clés personnalisés définis via Config (catégories ajoutées par l'utilisateur)
for (const [key, keywords] of Object.entries(CUSTOM_CATEGORY_KEYWORDS)) {
if (keywords.some(kw => kw && n.includes(kw))) return key;
}
return 'altro'; return 'altro';
} }
@@ -19856,6 +20025,9 @@ async function _initApp() {
const _startupOk = await _runStartupCheck(); const _startupOk = await _runStartupCheck();
if (!_startupOk) return; // preloader stays visible with error; app does not start if (!_startupOk) return; // preloader stays visible with error; app does not start
// Load custom categories (merges into CATEGORY_LABELS/CATEGORY_ICONS before any UI renders)
await loadCustomCategories();
// Load custom locations (merges into LOCATIONS before any UI renders) // Load custom locations (merges into LOCATIONS before any UI renders)
await loadCustomLocations(); await loadCustomLocations();