This commit is contained in:
1 parent
3c615e3021
commit
4cfef07dc6
22 files changed
+2918
-780
No files matched your search
@@ -0,0 +1,567 @@
|
||||
<?php
|
||||
// modules/food/views/liste.php
|
||||
if (!isset($pdo)) { exit; }
|
||||
?>
|
||||
|
||||
<div class="liste-main">
|
||||
<!-- Tabs des listes -->
|
||||
<div class="liste-tabs-bar">
|
||||
<div class="liste-tabs" id="liste-tabs"></div>
|
||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Corps de la liste active -->
|
||||
<div class="liste-body" id="liste-body">
|
||||
<div class="liste-add-card">
|
||||
<input type="text" id="liste-input" maxlength="500" placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
||||
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
||||
</div>
|
||||
|
||||
<div class="liste-toolbar" id="liste-toolbar">
|
||||
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
||||
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
||||
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
||||
</div>
|
||||
|
||||
<div id="liste-items-root"></div>
|
||||
<div id="liste-history-root"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const LISTE_TRANSLATIONS = {
|
||||
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
||||
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
||||
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
||||
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
||||
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
||||
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
||||
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
||||
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
||||
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
||||
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
||||
added: <?= json_encode(tr('liste_added')) ?>,
|
||||
updated: <?= json_encode(tr('liste_updated')) ?>,
|
||||
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
||||
cancel: <?= json_encode(tr('cancel')) ?>,
|
||||
};
|
||||
|
||||
// ⚠️ NOUVEAU CHEMIN D'API
|
||||
const API = '/modules/food/includes/api/api-liste.php';
|
||||
const T = window.LISTE_TRANSLATIONS || {};
|
||||
|
||||
const state = {
|
||||
lists: [],
|
||||
currentListId: null,
|
||||
items: [],
|
||||
history: [],
|
||||
categories: [],
|
||||
catMap: {},
|
||||
};
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const LIST_COLORS = [
|
||||
{ id: 'none', hex: null },
|
||||
{ id: 'blue', hex: '#3b82f6' },
|
||||
{ id: 'red', hex: '#ef4444' },
|
||||
{ id: 'amber', hex: '#f59e0b' },
|
||||
{ id: 'purple', hex: '#8b5cf6' },
|
||||
{ id: 'pink', hex: '#ec4899' },
|
||||
{ id: 'emerald', hex: '#10b981' },
|
||||
{ id: 'orange', hex: '#f97316' },
|
||||
{ id: 'cyan', hex: '#06b6d4' },
|
||||
{ id: 'indigo', hex: '#6366f1' },
|
||||
{ id: 'teal', hex: '#14b8a6' },
|
||||
{ id: 'gray', hex: '#6b7280' },
|
||||
];
|
||||
|
||||
const LIST_TYPES = [
|
||||
{ value: '', label: '— Aucun —', emoji: '' },
|
||||
{ value: 'courses', label: 'Courses', emoji: '🛒' },
|
||||
{ value: 'todo', label: 'To-do', emoji: '✅' },
|
||||
{ value: 'voyage', label: 'Voyage', emoji: '✈️' },
|
||||
{ value: 'travail', label: 'Travail', emoji: '💼' },
|
||||
{ value: 'maison', label: 'Maison', emoji: '🏠' },
|
||||
{ value: 'sante', label: 'Santé', emoji: '💊' },
|
||||
{ value: 'loisirs', label: 'Loisirs', emoji: '🎮' },
|
||||
{ value: 'autre', label: 'Autre', emoji: '📋' },
|
||||
];
|
||||
|
||||
// ── Utilities ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function toast(msg, type = 'info') {
|
||||
let c = document.getElementById('liste-toast-container');
|
||||
if (!c) {
|
||||
c = document.createElement('div');
|
||||
c.id = 'liste-toast-container';
|
||||
c.className = 'liste-toast-container';
|
||||
document.body.appendChild(c);
|
||||
}
|
||||
const t = document.createElement('div');
|
||||
t.className = `liste-toast liste-toast-${type}`;
|
||||
t.textContent = msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(() => t.classList.add('show'), 10);
|
||||
setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 300); }, 2500);
|
||||
}
|
||||
|
||||
async function api(action, method = 'GET', body = null, params = {}) {
|
||||
const url = new URL(API, location.origin);
|
||||
url.searchParams.set('action', action);
|
||||
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
||||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
const r = await fetch(url, opts);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// ── List modal ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let activeModal = null;
|
||||
|
||||
function openListModal(listId = null) {
|
||||
if (activeModal) { activeModal.remove(); activeModal = null; }
|
||||
const list = listId ? state.lists.find(l => l.id === listId) : null;
|
||||
const isNew = !listId;
|
||||
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'liste-modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="liste-modal" role="dialog" aria-modal="true">
|
||||
<div class="liste-modal-header">
|
||||
<h3>${isNew ? 'Nouvelle liste' : 'Modifier la liste'}</h3>
|
||||
<button class="liste-modal-close" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="liste-modal-body">
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Nom</label>
|
||||
<input class="liste-modal-name" type="text" maxlength="100"
|
||||
value="${esc(list?.name ?? '')}" placeholder="Ma liste…" autocomplete="off">
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Couleur</label>
|
||||
<div class="liste-color-picker">
|
||||
${LIST_COLORS.map(c => {
|
||||
const isActive = (c.hex === null && !list?.color) || (list?.color === c.hex);
|
||||
const cls = 'liste-color-swatch' + (c.hex === null ? ' liste-color-swatch-none' : '') + (isActive ? ' active' : '');
|
||||
const style = c.hex ? `style="--swatch:${c.hex}"` : '';
|
||||
return `<button class="${cls}" data-color="${c.hex ?? ''}" ${style} title="${c.id}"></button>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Type de liste</label>
|
||||
<select class="liste-modal-type">
|
||||
${LIST_TYPES.map(t =>
|
||||
`<option value="${esc(t.value)}" ${list?.list_type === t.value ? 'selected' : ''}>${t.emoji ? t.emoji + ' ' : ''}${esc(t.label)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-modal-footer">
|
||||
${!isNew && state.lists.length > 1
|
||||
? `<button class="btn-tool btn-tool-danger liste-modal-delete">Supprimer</button>`
|
||||
: ''}
|
||||
<button class="btn-tool liste-modal-cancel">Annuler</button>
|
||||
<button class="btn-tool btn-tool-primary liste-modal-save">Enregistrer</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(backdrop);
|
||||
activeModal = backdrop;
|
||||
|
||||
const nameInput = backdrop.querySelector('.liste-modal-name');
|
||||
nameInput.focus();
|
||||
nameInput.setSelectionRange(nameInput.value.length, nameInput.value.length);
|
||||
|
||||
// Color swatches
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const close = () => { backdrop.remove(); activeModal = null; };
|
||||
backdrop.querySelector('.liste-modal-close').addEventListener('click', close);
|
||||
backdrop.querySelector('.liste-modal-cancel').addEventListener('click', close);
|
||||
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
|
||||
|
||||
backdrop.querySelector('.liste-modal-delete')?.addEventListener('click', () => { close(); deleteList(listId); });
|
||||
backdrop.querySelector('.liste-modal-save').addEventListener('click', () => saveListModal(backdrop, listId));
|
||||
nameInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') saveListModal(backdrop, listId);
|
||||
if (e.key === 'Escape') close();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveListModal(backdrop, listId) {
|
||||
const name = backdrop.querySelector('.liste-modal-name').value.trim();
|
||||
if (!name) { backdrop.querySelector('.liste-modal-name').focus(); return; }
|
||||
|
||||
const activeSwatch = backdrop.querySelector('.liste-color-swatch.active');
|
||||
const color = activeSwatch?.dataset.color || null;
|
||||
const list_type = backdrop.querySelector('.liste-modal-type').value || null;
|
||||
|
||||
if (listId) {
|
||||
const r = await api('lists', 'PUT', { name, color, list_type }, { id: listId });
|
||||
if (r.ok) {
|
||||
const list = state.lists.find(l => l.id === listId);
|
||||
if (list) { list.name = name; list.color = color; list.list_type = list_type; }
|
||||
backdrop.remove(); activeModal = null;
|
||||
renderTabs();
|
||||
toast('Liste mise à jour');
|
||||
}
|
||||
} else {
|
||||
const r = await api('lists', 'POST', { name, color, list_type });
|
||||
if (r.id) {
|
||||
state.lists.push({ id: r.id, name, color: r.color, list_type: r.list_type, position: r.position });
|
||||
state.currentListId = r.id;
|
||||
backdrop.remove(); activeModal = null;
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast('Liste créée : ' + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Categories ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadCategories() {
|
||||
if (state.categories.length) return;
|
||||
const r = await api('categories');
|
||||
state.categories = r.categories || [];
|
||||
state.catMap = {};
|
||||
state.categories.forEach(c => { state.catMap[c.id] = c; });
|
||||
}
|
||||
|
||||
let activePicker = null;
|
||||
|
||||
function openCategoryPicker(itemId, anchorEl) {
|
||||
closeCategoryPicker();
|
||||
const item = state.items.find(i => i.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
const picker = document.createElement('div');
|
||||
picker.className = 'liste-cat-picker';
|
||||
picker.innerHTML = `
|
||||
<div class="liste-cat-picker-title">Catégorie</div>
|
||||
<div class="liste-cat-picker-list">
|
||||
<button class="liste-cat-picker-item${!item.category_id ? ' active' : ''}" data-cat="0">🏷️ Non classé</button>
|
||||
${state.categories.map(c =>
|
||||
`<button class="liste-cat-picker-item${item.category_id == c.id ? ' active' : ''}" data-cat="${c.id}">${esc(c.icon)} ${esc(c.name)}</button>`
|
||||
).join('')}
|
||||
</div>`;
|
||||
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
let top = rect.bottom + 4;
|
||||
let left = rect.left;
|
||||
if (left + 220 > window.innerWidth) left = window.innerWidth - 228;
|
||||
if (top + 340 > window.innerHeight) top = rect.top - Math.min(340, top + 340 - window.innerHeight) - 4;
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.left = left + 'px';
|
||||
|
||||
document.body.appendChild(picker);
|
||||
activePicker = picker;
|
||||
|
||||
picker.querySelectorAll('.liste-cat-picker-item').forEach(btn => {
|
||||
btn.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const catId = parseInt(btn.dataset.cat) || null;
|
||||
await api('set_category', 'POST', { item_id: itemId, category_id: catId });
|
||||
const it = state.items.find(i => i.id === itemId);
|
||||
if (it) it.category_id = catId;
|
||||
closeCategoryPicker();
|
||||
renderItems();
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeCategoryPicker, { once: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function closeCategoryPicker() {
|
||||
if (activePicker) { activePicker.remove(); activePicker = null; }
|
||||
}
|
||||
|
||||
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function typeEmoji(list_type) {
|
||||
return LIST_TYPES.find(t => t.value === list_type)?.emoji || '';
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const container = document.getElementById('liste-tabs');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
state.lists.forEach(list => {
|
||||
const isActive = list.id === state.currentListId;
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'liste-tab' + (isActive ? ' active' : '');
|
||||
tab.dataset.id = list.id;
|
||||
|
||||
// Apply custom color to entire tab
|
||||
if (list.color) {
|
||||
if (isActive) {
|
||||
tab.style.cssText = `background:${list.color};border-color:${list.color};color:#fff`;
|
||||
} else {
|
||||
tab.style.cssText = `border-color:${list.color};color:${list.color}`;
|
||||
}
|
||||
}
|
||||
|
||||
const emoji = typeEmoji(list.list_type);
|
||||
const prefix = emoji ? `<span class="liste-tab-type">${emoji}</span>` : '';
|
||||
|
||||
if (isActive) {
|
||||
tab.innerHTML = `
|
||||
${prefix}
|
||||
<span class="liste-tab-name">${esc(list.name)}</span>
|
||||
<button class="liste-tab-btn liste-tab-edit" title="Modifier" data-id="${list.id}">✏</button>
|
||||
`;
|
||||
} else {
|
||||
tab.innerHTML = `${prefix}<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||
tab.addEventListener('click', () => switchList(list.id));
|
||||
}
|
||||
container.appendChild(tab);
|
||||
});
|
||||
|
||||
container.querySelectorAll('.liste-tab-edit').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
|
||||
});
|
||||
}
|
||||
|
||||
// ── List management ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-add-list')?.addEventListener('click', () => openListModal(null));
|
||||
|
||||
async function deleteList(listId) {
|
||||
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
||||
const r = await api('lists', 'DELETE', null, { id: listId });
|
||||
if (r.error) { toast(r.error, 'error'); return; }
|
||||
state.lists = state.lists.filter(l => l.id !== listId);
|
||||
if (state.currentListId === listId) {
|
||||
state.currentListId = state.lists[0]?.id ?? null;
|
||||
}
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast(T.list_deleted || 'Liste supprimée');
|
||||
}
|
||||
|
||||
// ── Switch list ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function switchList(listId) {
|
||||
state.currentListId = listId;
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
}
|
||||
|
||||
// ── Items ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadItems() {
|
||||
if (!state.currentListId) { renderItems(); return; }
|
||||
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
||||
state.items = r.items || [];
|
||||
renderItems();
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
const root = document.getElementById('liste-items-root');
|
||||
if (!root) return;
|
||||
const toolbar = document.getElementById('liste-toolbar');
|
||||
if (!state.currentListId || state.items.length === 0) {
|
||||
root.innerHTML = state.currentListId
|
||||
? `<div class="liste-empty-items"><span>📝</span><p>Liste vide — ajoutez un article ci-dessus.</p></div>`
|
||||
: '';
|
||||
if (toolbar) toolbar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (toolbar) toolbar.style.display = '';
|
||||
|
||||
const pending = state.items.filter(i => !i.in_cart);
|
||||
const inCart = state.items.filter(i => i.in_cart);
|
||||
const hasCategories = pending.some(i => i.category_id);
|
||||
|
||||
let html = '';
|
||||
|
||||
if (hasCategories && state.categories.length) {
|
||||
const groups = new Map();
|
||||
pending.forEach(item => {
|
||||
const key = item.category_id ?? 0;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(item);
|
||||
});
|
||||
const catIds = state.categories.map(c => parseInt(c.id));
|
||||
const orderedKeys = catIds.filter(id => groups.has(id));
|
||||
if (groups.has(0)) orderedKeys.push(0);
|
||||
|
||||
orderedKeys.forEach(key => {
|
||||
const items = groups.get(key);
|
||||
const cat = key ? state.catMap[key] : null;
|
||||
const label = cat ? `${cat.icon} ${cat.name}` : '🏷️ Non classé';
|
||||
html += `<div class="liste-cat-section-header">${esc(label)} <span class="liste-cat-section-count">${items.length}</span></div>`;
|
||||
html += items.map(rowHtml).join('');
|
||||
});
|
||||
} else {
|
||||
html = pending.map(rowHtml).join('');
|
||||
}
|
||||
|
||||
if (inCart.length) {
|
||||
html += `<div class="liste-cart-divider">Dans le panier (${inCart.length})</div>` + inCart.map(rowHtml).join('');
|
||||
}
|
||||
|
||||
root.innerHTML = html;
|
||||
|
||||
root.querySelectorAll('.liste-check').forEach(cb => {
|
||||
cb.addEventListener('change', () => toggleCart(parseInt(cb.dataset.id), cb.checked ? 1 : 0));
|
||||
});
|
||||
root.querySelectorAll('.btn-edit-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => editItem(parseInt(btn.dataset.id)));
|
||||
});
|
||||
root.querySelectorAll('.btn-delete-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteItem(parseInt(btn.dataset.id)));
|
||||
});
|
||||
root.querySelectorAll('.liste-cat-badge').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); openCategoryPicker(parseInt(btn.dataset.id), btn); });
|
||||
});
|
||||
}
|
||||
|
||||
function rowHtml(item) {
|
||||
const checked = item.in_cart ? 'checked' : '';
|
||||
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
||||
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
||||
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
||||
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
||||
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
||||
return `<div class="${cls}" data-id="${item.id}">
|
||||
<button class="${badgeCls}" data-id="${item.id}" title="${badgeTip}">${badgeIcon}</button>
|
||||
<label class="liste-check-label">
|
||||
<input type="checkbox" class="liste-check" data-id="${item.id}" ${checked}>
|
||||
<span class="liste-label">${esc(item.label)}</span>
|
||||
</label>
|
||||
<div class="liste-row-actions">
|
||||
<button class="btn-edit-item" data-id="${item.id}" title="Modifier">✏</button>
|
||||
<button class="btn-delete-item" data-id="${item.id}" title="Supprimer">🗑</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function addItem(label) {
|
||||
const r = await api('items', 'POST', { list_id: state.currentListId, label });
|
||||
if (r.duplicate) { toast(T.already_in || 'Déjà dans la liste', 'warn'); return; }
|
||||
if (r.id) {
|
||||
state.items.push(r);
|
||||
renderItems();
|
||||
renderHistory();
|
||||
toast(T.added || 'Ajouté');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCart(id, next) {
|
||||
await api('items', 'PUT', { in_cart: next }, { id });
|
||||
const item = state.items.find(i => i.id === id);
|
||||
if (item) item.in_cart = next;
|
||||
renderItems();
|
||||
}
|
||||
|
||||
async function editItem(id) {
|
||||
const item = state.items.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
const newLabel = prompt(T.edit_placeholder || 'Modifier :', item.label);
|
||||
if (newLabel === null || newLabel.trim() === '') return;
|
||||
await api('items', 'PUT', { label: newLabel.trim() }, { id });
|
||||
item.label = newLabel.trim();
|
||||
renderItems();
|
||||
toast(T.updated || 'Modifié');
|
||||
}
|
||||
|
||||
async function deleteItem(id) {
|
||||
await api('items', 'DELETE', null, { id });
|
||||
state.items = state.items.filter(i => i.id !== id);
|
||||
renderItems();
|
||||
toast(T.deleted || 'Supprimé');
|
||||
}
|
||||
|
||||
// ── Toolbar ────────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-uncheck-all')?.addEventListener('click', async () => {
|
||||
if (!state.currentListId) return;
|
||||
await api('uncheck_all', 'POST', { list_id: state.currentListId });
|
||||
state.items.forEach(i => i.in_cart = 0);
|
||||
renderItems();
|
||||
});
|
||||
|
||||
document.getElementById('btn-delete-picked')?.addEventListener('click', async () => {
|
||||
if (!state.currentListId) return;
|
||||
await api('delete_picked', 'POST', { list_id: state.currentListId });
|
||||
state.items = state.items.filter(i => !i.in_cart);
|
||||
renderItems();
|
||||
toast(T.deleted || 'Supprimés');
|
||||
});
|
||||
|
||||
document.getElementById('btn-clear-all')?.addEventListener('click', async () => {
|
||||
if (!confirm(T.confirm_clear || 'Vider toute la liste ?')) return;
|
||||
await api('clear_all', 'POST', { list_id: state.currentListId });
|
||||
state.items = [];
|
||||
renderItems();
|
||||
toast(T.deleted || 'Liste vidée');
|
||||
});
|
||||
|
||||
// ── Add input ───────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-liste-add')?.addEventListener('click', () => {
|
||||
const input = document.getElementById('liste-input');
|
||||
const v = input?.value.trim();
|
||||
if (v) { addItem(v); input.value = ''; input.focus(); }
|
||||
});
|
||||
|
||||
document.getElementById('liste-input')?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('btn-liste-add')?.click();
|
||||
});
|
||||
|
||||
// ── History ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadHistory() {
|
||||
const r = await api('history');
|
||||
state.history = r.history || [];
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const root = document.getElementById('liste-history-root');
|
||||
if (!root || !state.history.length) { if (root) root.innerHTML = ''; return; }
|
||||
const currentLabels = new Set(state.items.map(i => i.label.toLowerCase().trim()));
|
||||
const suggestions = state.history.filter(h => !currentLabels.has(h.toLowerCase().trim()));
|
||||
if (!suggestions.length) { root.innerHTML = ''; return; }
|
||||
root.innerHTML = `
|
||||
<div class="liste-history-card">
|
||||
<div class="liste-history-title">${T.history_title || 'Récemment utilisés'}</div>
|
||||
<div class="liste-history-chips">
|
||||
${suggestions.map(h => `<button class="liste-chip" data-label="${esc(h)}">${esc(h)}</button>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
root.querySelectorAll('.liste-chip').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (state.currentListId) addItem(btn.dataset.label);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function init() {
|
||||
const [listsR] = await Promise.all([api('lists'), loadCategories()]);
|
||||
state.lists = listsR.lists || [];
|
||||
state.currentListId = listsR.default_id || state.lists[0]?.id || null;
|
||||
renderTabs();
|
||||
await Promise.all([loadItems(), loadHistory()]);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
// modules/meals/main.php
|
||||
|
||||
// Récupération stricte des membres du foyer (Exclusion des intervenants et proches)
|
||||
$stmt = $pdo->query("SELECT id, name FROM pf_people WHERE role NOT IN ('helper', 'nounou', 'proche_adulte', 'proche_enfant', 'relative') AND is_active = 1 ORDER BY id ASC");
|
||||
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($members)) {
|
||||
echo "<div class='pf-alert pf-alert--warning'>" . tr('meals_no_members') . "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcul des 14 jours (Lundi semaine courante -> Dimanche semaine prochaine)
|
||||
$start_date = new DateTime();
|
||||
$start_date->modify('monday this week');
|
||||
$dates = [];
|
||||
for ($i = 0; $i < 14; $i++) {
|
||||
$d = clone $start_date;
|
||||
$d->modify("+$i days");
|
||||
$dates[] = $d->format('Y-m-d');
|
||||
}
|
||||
|
||||
$stmtMeals = $pdo->prepare("SELECT plan_date, service, person_id, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ?");
|
||||
$stmtMeals->execute([$dates[0], end($dates)]);
|
||||
$mealsData = [];
|
||||
while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
||||
$mealsData[$row['plan_date']][$row['service']][$row['person_id']] = $row['meal_name'];
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="pf-container meals-container">
|
||||
<div class="meals-header-bar">
|
||||
<h1 class="meals-title">
|
||||
<span class="meals-title-icon">🍽️</span>
|
||||
<span data-i18n="meals_board_title"><?= tr('meals_board_title') ?></span>
|
||||
</h1>
|
||||
<div class="meals-actions">
|
||||
<!-- NOUVEAU : Bouton Import PDF -->
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.add('open'); document.body.classList.add('no-scroll');">
|
||||
📄 <?= tr('meal_btn_import_pdf') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre contextuelle (Fixe en bas) -->
|
||||
<div class="context-toolbar" id="contextToolbar">
|
||||
<button class="ctx-btn" onmousedown="event.preventDefault(); applyToAllInRow()">➡️ <span data-i18n="btn_apply_all"><?= tr('btn_apply_all') ?></span></button>
|
||||
<button class="ctx-btn primary" onmousedown="event.preventDefault(); openCopySheetFromCell()">🔄 <span data-i18n="btn_copy_to"><?= tr('btn_copy_to') ?></span></button>
|
||||
</div>
|
||||
|
||||
<div class="meals-wrapper">
|
||||
<?php
|
||||
$weeks = array_chunk($dates, 7);
|
||||
foreach ($weeks as $weekIndex => $weekDates):
|
||||
$startWeekStr = date('d/m', strtotime($weekDates[0]));
|
||||
$endWeekStr = date('d/m', strtotime(end($weekDates)));
|
||||
?>
|
||||
<h2 class="meals-week-title"><?= tr('meal_week') ?> (<?= $startWeekStr ?> - <?= $endWeekStr ?>)</h2>
|
||||
<div class="meals-table-container">
|
||||
<table class="meals-table" id="mealsTable_<?= $weekIndex ?>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sticky"></th>
|
||||
<?php foreach ($members as $m): ?>
|
||||
<th><?= htmlspecialchars(strtoupper($m['name'])) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($weekDates as $dateStr):
|
||||
$dayKey = strtolower(date('D', strtotime($dateStr)));
|
||||
$dayName = tr('day_' . $dayKey);
|
||||
$dayDate = date('d/m', strtotime($dateStr));
|
||||
$fullDayName = tr('day_full_' . $dayKey) . ' ' . $dayDate;
|
||||
?>
|
||||
<tr class="row-lunch">
|
||||
<td class="col-sticky">
|
||||
<span class="day-label"><?= $dayName ?> <?= $dayDate ?></span>
|
||||
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'lunch', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_lunch') ?>">☀️</div>
|
||||
</td>
|
||||
<?php foreach ($members as $m):
|
||||
$val = $mealsData[$dateStr]['lunch'][$m['id']] ?? '';
|
||||
?>
|
||||
<td class="meal-input-cell">
|
||||
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="lunch" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
<tr class="row-dinner">
|
||||
<td class="col-sticky">
|
||||
<span class="day-label invisible"><?= $dayName ?> <?= $dayDate ?></span>
|
||||
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'dinner', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_dinner') ?>">🌙</div>
|
||||
</td>
|
||||
<?php foreach ($members as $m):
|
||||
$val = $mealsData[$dateStr]['dinner'][$m['id']] ?? '';
|
||||
?>
|
||||
<td class="meal-input-cell">
|
||||
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="dinner" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Sheet de Copie (Format Grille Ultra-compacte) -->
|
||||
<div id="copyModal" class="pf-modal pf-bottom-sheet">
|
||||
<div class="pf-modal-content pf-bs-content copy-modal-large">
|
||||
<div class="bs-header">
|
||||
<h3 class="bs-title">🔄 <span data-i18n="modal_copy_title"><?= tr('modal_copy_title') ?></span></h3>
|
||||
<button class="bs-close" onclick="closeCopyModal()">×</button>
|
||||
</div>
|
||||
<div id="copySourceLabel" class="meals-copy-source">Source : ...</div>
|
||||
|
||||
<div class="week-section-title" data-i18n="week_current"><?= tr('week_current') ?></div>
|
||||
<div class="copy-days-grid">
|
||||
<?php for($i=0; $i<7; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||
<div class="copy-day-card">
|
||||
<div class="copy-day-name"><?= $dName ?></div>
|
||||
<div class="copy-day-actions">
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<div class="week-section-title week-section-mt" data-i18n="week_next"><?= tr('week_next') ?></div>
|
||||
<div class="copy-days-grid">
|
||||
<?php for($i=7; $i<14; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||
<div class="copy-day-card">
|
||||
<div class="copy-day-name"><?= $dName ?></div>
|
||||
<div class="copy-day-actions">
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NOUVEAU : Modale d'import PDF Cantine -->
|
||||
<div id="importMenuModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 15px; margin-bottom: 20px;">
|
||||
<h3 class="pf-modal-title" style="margin: 0; border: none; padding: 0;">📄 <?= tr('meal_import_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-muted);" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');">×</button>
|
||||
</div>
|
||||
<form id="importMenuForm" onsubmit="importPdfMenu(event)" action="/modules/food/includes/api/import-pdf.php" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('meal_import_child') ?></label>
|
||||
<select name="person_id" class="pf-input" required>
|
||||
<option value="" disabled selected>-- <?= tr('meal_select_child') ?> --</option>
|
||||
<?php foreach ($members as $m): ?>
|
||||
<option value="<?= $m['id'] ?>"><?= htmlspecialchars($m['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||
<span class="meal-dropzone-icon">📥</span>
|
||||
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
||||
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required onchange="document.getElementById('pdfFileName').innerText = this.files[0] ? this.files[0].name : '<?= addslashes(tr('meal_drop_pdf')) ?>';">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn" id="btnImportPdf"><?= tr('btn_import') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Sauvegarde automatique AJAX à chaque modification
|
||||
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||
input.addEventListener('change', async function() {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_cell');
|
||||
fd.append('plan_date', this.dataset.d);
|
||||
fd.append('service', this.dataset.s);
|
||||
fd.append('person_id', this.dataset.p);
|
||||
fd.append('meal_name', this.value);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
flashSuccess(this);
|
||||
} catch(e) {
|
||||
console.error("Save error", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Focus Context Bar
|
||||
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
||||
|
||||
document.addEventListener('focusin', (e) => {
|
||||
if(e.target.classList.contains('js-meal-input')) {
|
||||
copyState = {
|
||||
type: 'cell',
|
||||
sourceDate: e.target.getAttribute('data-d'),
|
||||
service: e.target.getAttribute('data-s'),
|
||||
personIdx: e.target.getAttribute('data-p'),
|
||||
sourceElements: [e.target],
|
||||
fullDayName: ''
|
||||
};
|
||||
document.getElementById('contextToolbar').classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('focusout', (e) => {
|
||||
setTimeout(() => {
|
||||
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
||||
document.getElementById('contextToolbar').classList.remove('active');
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Actions Toolbar
|
||||
window.applyToAllInRow = async function() {
|
||||
if(copyState.type !== 'cell' || !copyState.sourceElements[0].value.trim()) return;
|
||||
const val = copyState.sourceElements[0].value;
|
||||
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${copyState.sourceDate}"][data-s="${copyState.service}"]`);
|
||||
|
||||
let updates = [];
|
||||
inputs.forEach(inp => {
|
||||
if(inp !== copyState.sourceElements[0]) {
|
||||
inp.value = val;
|
||||
flashSuccess(inp);
|
||||
updates.push({ plan_date: inp.dataset.d, service: inp.dataset.s, person_id: inp.dataset.p, meal_name: val });
|
||||
}
|
||||
});
|
||||
|
||||
if (updates.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_bulk');
|
||||
fd.append('updates', JSON.stringify(updates));
|
||||
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
}
|
||||
};
|
||||
|
||||
window.openCopySheetFromCell = function() {
|
||||
if(!copyState.sourceElements[0] || !copyState.sourceElements[0].value.trim()) return;
|
||||
if(document.activeElement) document.activeElement.blur();
|
||||
|
||||
const svcLabel = copyState.service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||
const dObj = new Date(copyState.sourceDate);
|
||||
const dayKey = dObj.toLocaleDateString('en-US', {weekday: 'short'}).toLowerCase();
|
||||
const dayName = window.I18N['day_full_' + dayKey] || dObj.toLocaleDateString();
|
||||
|
||||
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_individual'] || 'Individuel'} (${dayName} ${svcLabel})`;
|
||||
|
||||
document.getElementById('copyModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
};
|
||||
|
||||
window.openCopySheetFromRow = function(dateStr, service, fullDayName) {
|
||||
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${dateStr}"][data-s="${service}"]`);
|
||||
if (!Array.from(inputs).some(inp => inp.value.trim() !== '')) return;
|
||||
|
||||
copyState = { type: 'row', sourceDate: dateStr, service: service, sourceElements: Array.from(inputs), fullDayName: fullDayName };
|
||||
const svcLabel = service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_row'] || 'Repas commun'} (${fullDayName} ${svcLabel})`;
|
||||
|
||||
document.getElementById('copyModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
};
|
||||
|
||||
window.closeCopyModal = function() {
|
||||
document.getElementById('copyModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
|
||||
window.executeCopy = async function(targetDate, targetService) {
|
||||
let updates = [];
|
||||
|
||||
if(copyState.type === 'cell') {
|
||||
const val = copyState.sourceElements[0].value;
|
||||
const targetInput = document.querySelector(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`);
|
||||
if(targetInput) {
|
||||
targetInput.value = val;
|
||||
flashSuccess(targetInput);
|
||||
updates.push({ plan_date: targetDate, service: targetService, person_id: copyState.personIdx, meal_name: val });
|
||||
}
|
||||
} else if (copyState.type === 'row') {
|
||||
const targetInputs = document.querySelectorAll(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"]`);
|
||||
copyState.sourceElements.forEach((inp, idx) => {
|
||||
if(inp.value.trim() !== '') {
|
||||
targetInputs[idx].value = inp.value;
|
||||
flashSuccess(targetInputs[idx]);
|
||||
updates.push({ plan_date: targetDate, service: targetService, person_id: targetInputs[idx].dataset.p, meal_name: inp.value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeCopyModal();
|
||||
|
||||
if (updates.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_bulk');
|
||||
fd.append('updates', JSON.stringify(updates));
|
||||
await pachaFetch('/modules/meals/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
}
|
||||
};
|
||||
|
||||
function flashSuccess(input) {
|
||||
input.style.transition = 'background 0.3s';
|
||||
input.style.background = 'var(--alert-success-bg)';
|
||||
setTimeout(() => input.style.background = 'transparent', 400);
|
||||
}
|
||||
|
||||
// NOUVEAU : Fonction Javascript d'Import PDF
|
||||
window.importPdfMenu = async function(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const btn = document.getElementById('btnImportPdf');
|
||||
const oldText = btn.innerText;
|
||||
btn.disabled = true;
|
||||
btn.innerText = '⏳...';
|
||||
|
||||
const fd = new FormData(form);
|
||||
const actionUrl = form.getAttribute('action');
|
||||
|
||||
try {
|
||||
const res = await pachaFetch(actionUrl, { method: 'POST', body: fd });
|
||||
if (res && res.success) {
|
||||
document.getElementById('importMenuModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
if (window.showToast) showToast(window.I18N['meal_import_success'] || 'Menus importés !', 'success');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
if (window.showToast) showToast(res.error || window.I18N['error_occured'], 'error');
|
||||
else alert(res.error);
|
||||
}
|
||||
} catch(err) {
|
||||
if (window.showToast) showToast(window.I18N['error_occured'], 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = oldText;
|
||||
form.reset();
|
||||
document.getElementById('pdfFileName').innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in new issue
Block a user