diff --git a/docker/schema_family.sql b/docker/schema_family.sql
index 4fe506c..4d7e727 100644
--- a/docker/schema_family.sql
+++ b/docker/schema_family.sql
@@ -374,9 +374,11 @@ CREATE TABLE IF NOT EXISTS pf_todos (
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_lists (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
- position INT NOT NULL DEFAULT 0,
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
+ color VARCHAR(20) DEFAULT NULL,
+ list_type VARCHAR(50) DEFAULT NULL,
+ position INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/liste.php b/liste.php
index 1956885..2b2d936 100644
--- a/liste.php
+++ b/liste.php
@@ -18,14 +18,6 @@ require __DIR__ . '/header.php';
-
-
diff --git a/modules/liste/api.php b/modules/liste/api.php
index 11f00b8..c82fd82 100644
--- a/modules/liste/api.php
+++ b/modules/liste/api.php
@@ -15,6 +15,7 @@ require_once dirname(__DIR__, 2) . '/includes/db.php';
// ── Auto-create tables ────────────────────────────────────────────────────────
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
+ color VARCHAR(20) DEFAULT NULL, list_type VARCHAR(50) DEFAULT NULL,
position INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
@@ -48,6 +49,8 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS pf_item_category_rules (
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
+try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN color VARCHAR(20) DEFAULT NULL AFTER name"); } catch (\Exception $e) {}
+try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN list_type VARCHAR(50) DEFAULT NULL AFTER color"); } catch (\Exception $e) {}
// ── Seed categories if empty ──────────────────────────────────────────────────
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
@@ -313,23 +316,32 @@ if ($action === 'set_category' && $method === 'POST') {
if ($action === 'lists') {
if ($method === 'GET') {
$firstId = liste_ensure_default($pdo);
- $rows = $pdo->query("SELECT id, name, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
+ $rows = $pdo->query("SELECT id, name, color, list_type, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
exit;
}
if ($method === 'POST') {
- $name = mb_substr(trim($body['name'] ?? ''), 0, 100);
+ $name = mb_substr(trim($body['name'] ?? ''), 0, 100);
+ $color = mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null;
+ $list_type = mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null;
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
- $pdo->prepare("INSERT INTO pf_lists (name, position) VALUES (?,?)")->execute([$name, $pos]);
- echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'position' => $pos]);
+ $pdo->prepare("INSERT INTO pf_lists (name, color, list_type, position) VALUES (?,?,?,?)")->execute([$name, $color, $list_type, $pos]);
+ echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'color' => $color, 'list_type' => $list_type, 'position' => $pos]);
exit;
}
if ($method === 'PUT') {
- $id = (int)($_GET['id'] ?? 0);
- $name = mb_substr(trim($body['name'] ?? ''), 0, 100);
+ $id = (int)($_GET['id'] ?? 0);
+ $name = mb_substr(trim($body['name'] ?? ''), 0, 100);
+ $color = array_key_exists('color', $body) ? (mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null) : false;
+ $list_type = array_key_exists('list_type', $body) ? (mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null) : false;
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
- $pdo->prepare("UPDATE pf_lists SET name=? WHERE id=?")->execute([$name, $id]);
+ $sets = ['name=?'];
+ $vals = [$name];
+ if ($color !== false) { $sets[] = 'color=?'; $vals[] = $color; }
+ if ($list_type !== false) { $sets[] = 'list_type=?'; $vals[] = $list_type; }
+ $vals[] = $id;
+ $pdo->prepare("UPDATE pf_lists SET " . implode(', ', $sets) . " WHERE id=?")->execute($vals);
echo json_encode(['ok' => true]); exit;
}
if ($method === 'DELETE') {
diff --git a/modules/liste/assets/liste.css b/modules/liste/assets/liste.css
index 445192a..8bacfb6 100644
--- a/modules/liste/assets/liste.css
+++ b/modules/liste/assets/liste.css
@@ -310,6 +310,142 @@
}
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
+/* ── List modal ────────────────────────────────────────────────────────────── */
+
+.liste-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+ padding: 1rem;
+}
+
+.liste-modal {
+ background: var(--card-bg, #fff);
+ border: 1px solid var(--border, #dee2e6);
+ border-radius: 14px;
+ width: 100%;
+ max-width: 420px;
+ box-shadow: 0 20px 60px rgba(0,0,0,.2);
+}
+
+.liste-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.25rem;
+ border-bottom: 1px solid var(--border, #dee2e6);
+}
+.liste-modal-header h3 { margin: 0; font-size: .95rem; font-weight: 700; color: var(--text, #212529); }
+
+.liste-modal-close {
+ background: none;
+ border: none;
+ font-size: 1.3rem;
+ cursor: pointer;
+ color: var(--muted, #6c757d);
+ line-height: 1;
+ padding: 0 .25rem;
+ transition: color .15s;
+}
+.liste-modal-close:hover { color: var(--text, #212529); }
+
+.liste-modal-body {
+ padding: 1.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: .85rem;
+}
+
+.liste-modal-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: .5rem;
+ padding: .85rem 1.25rem;
+ border-top: 1px solid var(--border, #dee2e6);
+}
+.liste-modal-footer .btn-tool-danger { margin-right: auto; }
+
+.liste-form-group { display: flex; flex-direction: column; gap: .3rem; }
+.liste-form-label {
+ font-size: .73rem;
+ font-weight: 700;
+ color: var(--muted, #6c757d);
+ text-transform: uppercase;
+ letter-spacing: .04em;
+}
+
+.liste-modal-name, .liste-modal-type {
+ padding: .5rem .7rem;
+ border: 1px solid var(--border, #dee2e6);
+ border-radius: 8px;
+ background: var(--input-bg, #fff);
+ color: var(--text, #212529);
+ font-size: .875rem;
+ font-family: inherit;
+ width: 100%;
+ box-sizing: border-box;
+ transition: border-color .15s;
+}
+.liste-modal-name:focus, .liste-modal-type:focus {
+ outline: none;
+ border-color: var(--primary, #4361ee);
+ box-shadow: 0 0 0 3px rgba(67,97,238,.15);
+}
+
+.btn-tool-primary {
+ background: var(--primary, #4361ee);
+ color: #fff;
+ border-color: var(--primary, #4361ee);
+}
+.btn-tool-primary:hover { filter: brightness(1.1); background: var(--primary, #4361ee); }
+
+/* ── Color picker ──────────────────────────────────────────────────────────── */
+
+.liste-color-picker {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .45rem;
+}
+
+.liste-color-swatch {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ border: 3px solid transparent;
+ background: var(--swatch, transparent);
+ cursor: pointer;
+ transition: transform .1s;
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ box-sizing: border-box;
+}
+.liste-color-swatch:hover { transform: scale(1.2); }
+.liste-color-swatch.active {
+ outline-color: var(--swatch, var(--primary, #4361ee));
+ border-color: var(--card-bg, #fff);
+}
+.liste-color-swatch-none {
+ background: transparent !important;
+ border: 2px dashed var(--border, #dee2e6) !important;
+}
+.liste-color-swatch-none.active { outline-color: var(--primary, #4361ee); }
+
+/* ── Tab dot + type emoji ──────────────────────────────────────────────────── */
+
+.liste-tab-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ display: inline-block;
+}
+.liste-tab-type { font-size: .85rem; line-height: 1; }
+
/* ── Category section headers ──────────────────────────────────────────────── */
.liste-cat-section-header {
@@ -429,6 +565,13 @@
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
[data-theme='dark'] .liste-new-form input { background: #1e1e2e; color: #cdd6f4; border-color: #4361ee; }
[data-theme='dark'] .liste-new-form #btn-cancel-new-list { background: #2a2a3e; color: #cdd6f4; }
+[data-theme='dark'] .liste-modal { background: #1e1e2e; border-color: #3a3a4e; }
+[data-theme='dark'] .liste-modal-header,
+[data-theme='dark'] .liste-modal-footer { border-color: #3a3a4e; }
+[data-theme='dark'] .liste-modal-header h3 { color: #cdd6f4; }
+[data-theme='dark'] .liste-modal-name,
+[data-theme='dark'] .liste-modal-type { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
+[data-theme='dark'] .liste-color-swatch.active { border-color: #1e1e2e; }
[data-theme='dark'] .liste-cat-badge { border-color: #3a3a4e; }
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
[data-theme='dark'] .liste-cat-section-count { background: #2a2a3e; color: #8892b0; }
diff --git a/modules/liste/assets/liste.js b/modules/liste/assets/liste.js
index c1906b4..4dfdf49 100644
--- a/modules/liste/assets/liste.js
+++ b/modules/liste/assets/liste.js
@@ -11,6 +11,35 @@ const state = {
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) {
@@ -42,6 +71,116 @@ async function api(action, method = 'GET', body = null, params = {}) {
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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ 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() {
@@ -104,49 +243,45 @@ function closeCategoryPicker() {
// ── 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 tab = document.createElement('div');
tab.className = 'liste-tab' + (list.id === state.currentListId ? ' active' : '');
tab.dataset.id = list.id;
+ const dot = list.color ? `
` : '';
+ const emoji = typeEmoji(list.list_type);
+ const prefix = emoji ? `
${emoji}` : '';
+
if (list.id === state.currentListId) {
tab.innerHTML = `
+ ${dot}${prefix}
${esc(list.name)}
-
- ${state.lists.length > 1 ? `
` : ''}
+
`;
} else {
- tab.innerHTML = `
${esc(list.name)}`;
+ tab.innerHTML = `${dot}${prefix}
${esc(list.name)}`;
tab.addEventListener('click', () => switchList(list.id));
}
container.appendChild(tab);
});
- container.querySelectorAll('.liste-tab-rename').forEach(btn => {
- btn.addEventListener('click', e => { e.stopPropagation(); startRename(parseInt(btn.dataset.id)); });
- });
- container.querySelectorAll('.liste-tab-delete').forEach(btn => {
- btn.addEventListener('click', e => { e.stopPropagation(); deleteList(parseInt(btn.dataset.id)); });
+ container.querySelectorAll('.liste-tab-edit').forEach(btn => {
+ btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
});
}
-function startRename(listId) {
- const list = state.lists.find(l => l.id === listId);
- if (!list) return;
- const name = prompt(T.new_name || 'Nouveau nom :', list.name);
- if (name === null || name.trim() === '') return;
- api('lists', 'PUT', { name: name.trim() }, { id: listId }).then(r => {
- if (r.ok) {
- list.name = name.trim();
- renderTabs();
- toast(T.list_renamed || 'Liste renommée');
- }
- });
-}
+// ── 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;
@@ -161,41 +296,6 @@ async function deleteList(listId) {
toast(T.list_deleted || 'Liste supprimée');
}
-// ── List management ─────────────────────────────────────────────────────────────
-
-document.getElementById('btn-add-list')?.addEventListener('click', () => {
- document.getElementById('liste-new-form')?.classList.remove('hidden');
- document.getElementById('btn-add-list')?.classList.add('hidden');
- document.getElementById('new-list-name')?.focus();
-});
-document.getElementById('btn-cancel-new-list')?.addEventListener('click', cancelNewList);
-document.getElementById('btn-confirm-new-list')?.addEventListener('click', confirmNewList);
-document.getElementById('new-list-name')?.addEventListener('keydown', e => {
- if (e.key === 'Enter') confirmNewList();
- if (e.key === 'Escape') cancelNewList();
-});
-
-function cancelNewList() {
- document.getElementById('liste-new-form')?.classList.add('hidden');
- document.getElementById('btn-add-list')?.classList.remove('hidden');
- if (document.getElementById('new-list-name')) document.getElementById('new-list-name').value = '';
-}
-
-async function confirmNewList() {
- const input = document.getElementById('new-list-name');
- const name = input?.value.trim();
- if (!name) return;
- const r = await api('lists', 'POST', { name });
- if (r.id) {
- state.lists.push({ id: r.id, name: r.name, position: r.position });
- state.currentListId = r.id;
- cancelNewList();
- await loadItems();
- renderTabs();
- toast((T.list_created || 'Liste créée') + ' : ' + r.name);
- }
-}
-
// ── Switch list ─────────────────────────────────────────────────────────────────
async function switchList(listId) {
@@ -207,10 +307,7 @@ async function switchList(listId) {
// ── Items ───────────────────────────────────────────────────────────────────────
async function loadItems() {
- if (!state.currentListId) {
- renderItems();
- return;
- }
+ if (!state.currentListId) { renderItems(); return; }
const r = await api('items', 'GET', null, { list_id: state.currentListId });
state.items = r.items || [];
renderItems();
@@ -236,15 +333,12 @@ function renderItems() {
let html = '';
if (hasCategories && state.categories.length) {
- // Group pending items by category
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);
});
-
- // Render in category order, uncategorized last
const catIds = state.categories.map(c => parseInt(c.id));
const orderedKeys = catIds.filter(id => groups.has(id));
if (groups.has(0)) orderedKeys.push(0);
@@ -284,7 +378,7 @@ 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 badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
const badgeTip = cat ? esc(cat.name) : 'Non classé';
return `