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
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:
@@ -1370,6 +1370,17 @@ function renderLocationButtons(containerId, activeKey, onClickFnName) {
|
||||
}).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() {
|
||||
const container = document.getElementById('locations-list-container');
|
||||
if (!container) return;
|
||||
@@ -1384,9 +1395,141 @@ async function _loadConfigPage() {
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
|
||||
}
|
||||
await _loadCategoriesConfigSection();
|
||||
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() {
|
||||
const catSelect = document.getElementById('subcat-config-category');
|
||||
if (!catSelect) return;
|
||||
@@ -1628,6 +1771,28 @@ const CATEGORY_ICONS = {
|
||||
'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
|
||||
const CATEGORY_LOCATION = {
|
||||
'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';
|
||||
// Pulizia casa
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -19856,6 +20025,9 @@ async function _initApp() {
|
||||
const _startupOk = await _runStartupCheck();
|
||||
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)
|
||||
await loadCustomLocations();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user