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('');
|
}).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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user