diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 7816274..f2eb354 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -317,3 +317,27 @@ CREATE TABLE IF NOT EXISTS pf_memo_attachments ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Todo ───────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_todo_lists ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + color VARCHAR(20) DEFAULT '#3b82f6', + icon VARCHAR(10) DEFAULT '📋', + position INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + list_id INT DEFAULT NULL, + title VARCHAR(500) NOT NULL, + notes TEXT DEFAULT NULL, + due_date DATE DEFAULT NULL, + priority ENUM('none','low','medium','high') DEFAULT 'none', + done TINYINT(1) DEFAULT 0, + done_at DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/header.php b/header.php index ed8f0ca..c9b81b3 100644 --- a/header.php +++ b/header.php @@ -54,6 +54,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; = tr('menu_gifts') ?> = tr('menu_garage') ?> = tr('menu_memo') ?> + = tr('menu_todo') ?> @@ -100,6 +101,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; 🎁 = tr('menu_gifts') ?> 🚗 = tr('menu_garage') ?> 📝 = tr('menu_memo') ?> + ✅ = tr('menu_todo') ?> ⚙️ Paramètres 🛡️ Admin diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 04e195a..ac02120 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -93,6 +93,9 @@ return [ 'mod_memo_name' => 'Notes', 'mod_memo_desc' => 'Els vostres memos, receptes, instruccions i informació diversa amb Markdown.', 'menu_memo' => 'Notes', + 'mod_todo_name' => 'Todo', + 'mod_todo_desc' => 'Gestioneu les vostres tasques, llistes de la compra i recordatoris familiars.', + 'menu_todo' => 'Todo', 'cta_open' => 'Obrir', 'cta_explore' => 'Explorar', 'cta_view_lists' => 'Veure les llistes', diff --git a/includes/lang/en.php b/includes/lang/en.php index ebee33f..3328e01 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -94,6 +94,9 @@ return [ 'mod_memo_name' => 'Notes', 'mod_memo_desc' => 'Your memos, recipes, instructions and miscellaneous info with Markdown and attachments.', 'menu_memo' => 'Notes', + 'mod_todo_name' => 'Todo', + 'mod_todo_desc' => 'Manage tasks, shopping lists and family reminders.', + 'menu_todo' => 'Todo', 'cta_open' => 'Open', 'cta_explore' => 'Explore', 'cta_view_lists' => 'View lists', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index a0dcb76..ffcbb94 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -96,6 +96,9 @@ return [ 'mod_memo_name' => 'Notes', 'mod_memo_desc' => 'Vos mémos, recettes, instructions et infos diverses avec Markdown et pièces jointes.', 'menu_memo' => 'Notes', + 'mod_todo_name' => 'Todo', + 'mod_todo_desc' => 'Gérez vos tâches, listes de courses et rappels en famille.', + 'menu_todo' => 'Todo', 'cta_open' => 'Ouvrir', 'cta_explore' => 'Explorer', 'cta_view_lists' => 'Voir les listes', diff --git a/index.php b/index.php index 72c7acb..35a6a3f 100644 --- a/index.php +++ b/index.php @@ -100,6 +100,15 @@ if ($_has_custom_bg): ?> + + + ✅ + = tr('mod_todo_name') ?> + = tr('mod_todo_desc') ?> + = tr('cta_open') ?> + + + diff --git a/modules/todo/api.php b/modules/todo/api.php new file mode 100644 index 0000000..86c5e7f --- /dev/null +++ b/modules/todo/api.php @@ -0,0 +1,148 @@ + false, 'error' => $e->getMessage()]); exit; +}); + +require_once dirname(__DIR__, 2) . '/includes/db.php'; + +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? ''; + +function tOk($d) { echo json_encode(['ok' => true, 'data' => $d], JSON_UNESCAPED_UNICODE); exit; } +function tErr($m, $c = 400) { http_response_code($c); echo json_encode(['ok' => false, 'error' => $m]); exit; } +function tBody() { return json_decode(file_get_contents('php://input'), true) ?? []; } + +// ── LISTS ────────────────────────────────────────────────────────────────────── +if ($action === 'lists') { + if ($method === 'GET') { + $rows = $pdo->query( + "SELECT l.*, COUNT(t.id) as total, + SUM(CASE WHEN t.done=0 THEN 1 ELSE 0 END) as pending + FROM pf_todo_lists l + LEFT JOIN pf_todos t ON t.list_id = l.id + GROUP BY l.id ORDER BY l.position ASC, l.id ASC" + )->fetchAll(); + tOk($rows); + } + if ($method === 'POST') { + $d = tBody(); + $name = trim($d['name'] ?? ''); if (!$name) tErr('Nom requis'); + $pdo->prepare("INSERT INTO pf_todo_lists (name, color, icon) VALUES (?,?,?)") + ->execute([$name, $d['color'] ?? '#3b82f6', $d['icon'] ?? '📋']); + tOk(['id' => (int)$pdo->lastInsertId()]); + } + if ($method === 'PUT') { + $id = $_GET['id'] ?? null; if (!$id) tErr('ID manquant'); + $d = tBody(); + $name = trim($d['name'] ?? ''); if (!$name) tErr('Nom requis'); + $pdo->prepare("UPDATE pf_todo_lists SET name=?, color=?, icon=? WHERE id=?") + ->execute([$name, $d['color'] ?? '#3b82f6', $d['icon'] ?? '📋', $id]); + tOk(['updated' => true]); + } + if ($method === 'DELETE') { + $id = $_GET['id'] ?? null; if (!$id) tErr('ID manquant'); + $pdo->prepare("DELETE FROM pf_todo_lists WHERE id=?")->execute([$id]); + tOk(['deleted' => true]); + } +} + +// ── TODOS ────────────────────────────────────────────────────────────────────── +if ($action === 'todos') { + if ($method === 'GET') { + $list_id = $_GET['list_id'] ?? null; + $show_done = ($_GET['show_done'] ?? '0') === '1'; + $priority = $_GET['priority'] ?? null; + + $where = []; + $params = []; + + if ($list_id === 'all' || $list_id === null) { + // all + } elseif ($list_id === 'today') { + $where[] = 't.due_date = CURDATE()'; + $where[] = 't.done = 0'; + } elseif ($list_id === 'upcoming') { + $where[] = 't.due_date >= CURDATE()'; + $where[] = 't.done = 0'; + } else { + $where[] = 't.list_id = ?'; $params[] = $list_id; + } + + if (!$show_done && $list_id !== 'done') { + $where[] = 't.done = 0'; + } elseif ($list_id === 'done') { + $where[] = 't.done = 1'; + } + + if ($priority) { $where[] = 't.priority = ?'; $params[] = $priority; } + + $sql = "SELECT t.*, l.name as list_name, l.color as list_color, l.icon as list_icon + FROM pf_todos t LEFT JOIN pf_todo_lists l ON l.id = t.list_id"; + if ($where) $sql .= ' WHERE ' . implode(' AND ', $where); + $sql .= ' ORDER BY t.done ASC, FIELD(t.priority,"high","medium","low","none") ASC, t.due_date ASC, t.created_at ASC'; + + $stmt = $pdo->prepare($sql); $stmt->execute($params); + tOk($stmt->fetchAll()); + } + + if ($method === 'POST') { + $d = tBody(); + $title = trim($d['title'] ?? ''); if (!$title) tErr('Titre requis'); + $pdo->prepare("INSERT INTO pf_todos (list_id, title, notes, due_date, priority) VALUES (?,?,?,?,?)") + ->execute([ + $d['list_id'] ?: null, + $title, + $d['notes'] ?? null, + $d['due_date'] ?: null, + $d['priority'] ?? 'none' + ]); + $id = (int)$pdo->lastInsertId(); + $new = $pdo->prepare("SELECT t.*, l.name as list_name, l.color as list_color, l.icon as list_icon FROM pf_todos t LEFT JOIN pf_todo_lists l ON l.id = t.list_id WHERE t.id=?"); + $new->execute([$id]); tOk($new->fetch()); + } + + if ($method === 'PUT') { + $id = $_GET['id'] ?? null; if (!$id) tErr('ID manquant'); + $d = tBody(); + + // Toggle done + if (isset($d['done'])) { + $done = $d['done'] ? 1 : 0; + $pdo->prepare("UPDATE pf_todos SET done=?, done_at=?, updated_at=NOW() WHERE id=?") + ->execute([$done, $done ? date('Y-m-d H:i:s') : null, $id]); + tOk(['done' => $done]); + } + + // Full update + $title = trim($d['title'] ?? ''); if (!$title) tErr('Titre requis'); + $pdo->prepare("UPDATE pf_todos SET list_id=?, title=?, notes=?, due_date=?, priority=?, updated_at=NOW() WHERE id=?") + ->execute([$d['list_id'] ?: null, $title, $d['notes'] ?? null, $d['due_date'] ?: null, $d['priority'] ?? 'none', $id]); + tOk(['updated' => true]); + } + + if ($method === 'DELETE') { + $id = $_GET['id'] ?? null; if (!$id) tErr('ID manquant'); + $pdo->prepare("DELETE FROM pf_todos WHERE id=?")->execute([$id]); + tOk(['deleted' => true]); + } +} + +// ── STATS ────────────────────────────────────────────────────────────────────── +if ($action === 'stats') { + tOk([ + 'total' => $pdo->query("SELECT COUNT(*) FROM pf_todos")->fetchColumn(), + 'pending' => $pdo->query("SELECT COUNT(*) FROM pf_todos WHERE done=0")->fetchColumn(), + 'done' => $pdo->query("SELECT COUNT(*) FROM pf_todos WHERE done=1")->fetchColumn(), + 'today' => $pdo->query("SELECT COUNT(*) FROM pf_todos WHERE due_date=CURDATE() AND done=0")->fetchColumn(), + 'overdue' => $pdo->query("SELECT COUNT(*) FROM pf_todos WHERE due_date < CURDATE() AND done=0")->fetchColumn(), + ]); +} + +tErr('Action inconnue', 404); diff --git a/modules/todo/assets/todo.css b/modules/todo/assets/todo.css new file mode 100644 index 0000000..c420a81 --- /dev/null +++ b/modules/todo/assets/todo.css @@ -0,0 +1,230 @@ +/* Todo module */ + +.todo-layout { + display: grid; + grid-template-columns: 220px 1fr; + height: calc(100vh - 64px); + overflow: hidden; +} +.todo-sidebar { + border-right: 1px solid var(--border-light); + background: var(--bg-panel); + overflow-y: auto; + display: flex; + flex-direction: column; +} +.todo-sidebar-top { + padding: .75rem; + border-bottom: 1px solid var(--border-light); + position: sticky; top: 0; background: var(--bg-panel); z-index: 1; +} +.todo-main { overflow-y: auto; background: var(--bg-page); } + +/* Sidebar items */ +.todo-nav-item { + display: flex; align-items: center; gap: .6rem; + padding: .5rem .75rem; cursor: pointer; + font-size: .875rem; color: var(--text-muted); + border-radius: 8px; margin: 1px .5rem; + transition: background .12s, color .12s; + text-decoration: none; +} +.todo-nav-item:hover { background: var(--bg-page); color: var(--text-main); } +.todo-nav-item.active { background: #eff6ff; color: var(--primary); font-weight: 600; } +.todo-nav-badge { + margin-left: auto; font-size: .7rem; font-weight: 700; + background: var(--bg-page); color: var(--text-muted); + padding: 1px 6px; border-radius: 999px; min-width: 20px; text-align: center; +} +.todo-nav-item.active .todo-nav-badge { background: #dbeafe; color: var(--primary); } +.todo-nav-badge.urgent { background: #fee2e2; color: var(--danger); } +.todo-nav-section { font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: .75rem .75rem .25rem; } +.todo-list-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } + +/* Main header */ +.todo-main-header { + display: flex; align-items: center; justify-content: space-between; + padding: 1.1rem 1.5rem; background: var(--bg-panel); + border-bottom: 1px solid var(--border-light); gap: .75rem; flex-wrap: wrap; +} +.todo-main-header h2 { font-size: 1rem; font-weight: 700; margin: 0; } +.todo-subtitle { font-size: .8rem; color: var(--text-muted); } +.todo-header-actions { display: flex; gap: .5rem; align-items: center; } + +/* Quick add */ +.todo-quick-add { + display: flex; align-items: center; gap: .5rem; + margin: 1rem 1.5rem .75rem; + background: var(--bg-panel); border: 1px solid var(--border-light); + border-radius: 10px; padding: .5rem .75rem; + box-shadow: 0 1px 3px rgba(0,0,0,.06); + transition: border-color .15s, box-shadow .15s; +} +.todo-quick-add:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,.1); } +.todo-quick-add input { + border: none; outline: none; background: transparent; + font-size: .9rem; color: var(--text-main); flex: 1; +} +.todo-quick-add input::placeholder { color: var(--text-muted); } + +/* Todo list */ +.todo-list { padding: 0 1.5rem 2rem; } +.todo-group-label { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: .75rem 0 .35rem; } +.todo-item { + display: flex; align-items: flex-start; gap: .75rem; + padding: .65rem .9rem; margin-bottom: .35rem; + background: var(--bg-panel); border: 1px solid var(--border-light); + border-radius: 10px; cursor: pointer; + transition: border-color .15s, box-shadow .12s; + position: relative; +} +.todo-item:hover { border-color: #cbd5e1; box-shadow: 0 2px 8px rgba(0,0,0,.06); } +.todo-item.done { opacity: .55; } +.todo-item.done .todo-title { text-decoration: line-through; color: var(--text-muted); } + +/* Checkbox */ +.todo-check { + width: 20px; height: 20px; border-radius: 50%; + border: 2px solid #cbd5e1; background: transparent; + display: flex; align-items: center; justify-content: center; + cursor: pointer; flex-shrink: 0; margin-top: 2px; + transition: all .15s; +} +.todo-check:hover { border-color: var(--primary); background: #eff6ff; } +.todo-check.checked { background: var(--success); border-color: var(--success); color: #fff; font-size: .8rem; } +.todo-check.checked::after { content: '✓'; } + +/* Priority colors for checkbox */ +.todo-item[data-priority="high"] .todo-check { border-color: var(--danger); } +.todo-item[data-priority="high"] .todo-check:hover { background: #fef2f2; border-color: var(--danger); } +.todo-item[data-priority="medium"] .todo-check { border-color: var(--warning); } +.todo-item[data-priority="low"] .todo-check { border-color: var(--success); } + +.todo-content { flex: 1; min-width: 0; } +.todo-title { font-size: .9rem; font-weight: 500; color: var(--text-main); line-height: 1.3; } +.todo-meta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .25rem; } +.todo-due { font-size: .75rem; color: var(--text-muted); display: flex; align-items: center; gap: .2rem; } +.todo-due.overdue { color: var(--danger); font-weight: 600; } +.todo-due.today { color: #d97706; font-weight: 600; } +.todo-list-badge { font-size: .7rem; padding: .1rem .45rem; border-radius: 999px; font-weight: 500; } +.todo-priority-badge { font-size: .7rem; font-weight: 700; padding: .1rem .4rem; border-radius: 999px; } +.pri-high { background: #fee2e2; color: #dc2626; } +.pri-medium { background: #fef3c7; color: #b45309; } +.pri-low { background: #dcfce7; color: #15803d; } +.todo-notes-preview { font-size: .78rem; color: var(--text-muted); margin-top: .2rem; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } +.todo-item-actions { display: flex; gap: .25rem; opacity: 0; transition: opacity .12s; flex-shrink: 0; } +.todo-item:hover .todo-item-actions { opacity: 1; } + +/* Empty state */ +.todo-empty { text-align: center; padding: 4rem 2rem; color: var(--text-muted); } +.todo-empty .icon { font-size: 3rem; opacity: .3; margin-bottom: 1rem; } +.todo-empty p { margin: 0; } + +/* Modals */ +.todo-modal-backdrop { + display: none; position: fixed; inset: 0; z-index: 200; + background: rgba(15,23,42,.5); backdrop-filter: blur(4px); + align-items: center; justify-content: center; padding: 1rem; +} +.todo-modal-backdrop.show { display: flex; } +.todo-modal { + background: var(--bg-panel); border: 1px solid var(--border-light); + border-radius: 14px; width: 100%; max-width: 500px; + max-height: 90vh; display: flex; flex-direction: column; + box-shadow: 0 20px 40px rgba(0,0,0,.15); + animation: modalIn .18s ease; +} +@keyframes modalIn { from{opacity:0;transform:scale(.96) translateY(-8px)} to{opacity:1;transform:none} } +.todo-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-light); } +.todo-modal-header h3 { font-size: 1rem; font-weight: 700; margin: 0; } +.todo-modal-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.2rem; border-radius: 6px; padding: .2rem .4rem; } +.todo-modal-close:hover { background: var(--bg-page); } +.todo-modal-body { padding: 1.25rem; overflow-y: auto; display: flex; flex-direction: column; gap: .9rem; } +.todo-modal-footer { padding: 1rem 1.25rem; border-top: 1px solid var(--border-light); display: flex; justify-content: flex-end; gap: .5rem; } + +/* Form */ +.form-group { display: flex; flex-direction: column; gap: .35rem; } +.form-label { font-size: .8rem; font-weight: 600; color: var(--text-muted); } +.form-control { background: #fff; border: 1px solid var(--border-light); color: var(--text-main); border-radius: 8px; padding: .5rem .75rem; font-size: .875rem; width: 100%; outline: none; transition: border-color .15s, box-shadow .15s; font-family: inherit; } +.form-control:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,.1); } +textarea.form-control { resize: vertical; min-height: 80px; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; } + +/* Priority selector */ +.priority-opts { display: flex; gap: .4rem; } +.priority-opt { display: flex; align-items: center; gap: .35rem; padding: .35rem .65rem; border-radius: 8px; border: 1.5px solid var(--border-light); cursor: pointer; font-size: .8rem; font-weight: 600; transition: all .12s; } +.priority-opt:hover { border-color: #94a3b8; } +.priority-opt.sel-none { border-color: #cbd5e1; } +.priority-opt.sel-low { border-color: var(--success); background: #dcfce7; color: #15803d; } +.priority-opt.sel-medium { border-color: var(--warning); background: #fef3c7; color: #b45309; } +.priority-opt.sel-high { border-color: var(--danger); background: #fee2e2; color: #dc2626; } + +/* Color picker for list */ +.color-swatches { display: flex; gap: .4rem; flex-wrap: wrap; } +.color-swatch { width: 24px; height: 24px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: transform .12s; } +.color-swatch:hover { transform: scale(1.15); } +.color-swatch.selected { border-color: #0f172a; } + +/* Buttons */ +.btn { display: inline-flex; align-items: center; gap: .4rem; padding: .4rem .85rem; border-radius: 8px; font-size: .875rem; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all .15s; white-space: nowrap; } +.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); } +.btn-primary:hover { background: var(--primary-dark); } +.btn-secondary { background: var(--bg-panel); color: var(--text-main); border-color: var(--border-light); } +.btn-secondary:hover { background: var(--bg-page); } +.btn-danger { background: #fff; color: var(--danger); border-color: #fecaca; } +.btn-danger:hover { background: #fef2f2; } +.btn-sm { padding: .25rem .55rem; font-size: .78rem; } +.btn-icon { padding: .25rem; min-width: 28px; justify-content: center; } +.btn-ghost { background: transparent; border: none; color: var(--text-muted); padding: .2rem .4rem; } +.btn-ghost:hover { color: var(--text-main); background: var(--bg-page); border-radius: 6px; } + +/* Stats bar */ +.todo-stats { display: flex; gap: 1rem; padding: .5rem 1.5rem; background: var(--bg-panel); border-bottom: 1px solid var(--border-light); flex-wrap: wrap; } +.todo-stat-item { font-size: .78rem; color: var(--text-muted); display: flex; align-items: center; gap: .3rem; } +.todo-stat-item strong { color: var(--text-main); } + +/* Done animation */ +@keyframes doneCheck { from{transform:scale(1)} 40%{transform:scale(1.3)} to{transform:scale(1)} } +.todo-check.just-done { animation: doneCheck .3s ease; } + +/* Show done toggle */ +.show-done-btn { display: flex; align-items: center; gap: .4rem; font-size: .8rem; color: var(--text-muted); background: none; border: none; cursor: pointer; padding: .2rem .4rem; border-radius: 6px; } +.show-done-btn:hover { background: var(--bg-page); color: var(--text-main); } + +/* Toast */ +.todo-toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999; display: flex; flex-direction: column; gap: .5rem; } +.todo-toast { background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 10px; padding: .7rem 1rem; font-size: .875rem; min-width: 200px; color: var(--text-main); box-shadow: 0 4px 16px rgba(0,0,0,.12); border-left: 4px solid var(--primary); animation: toastIn .2s ease; } +.todo-toast.error { border-left-color: var(--danger); } +@keyframes toastIn { from{opacity:0;transform:translateX(16px)} to{opacity:1;transform:none} } + +/* Responsive */ +@media (max-width: 768px) { + .todo-layout { grid-template-columns: 1fr; height: auto; } + .todo-sidebar { display: none; } + .form-row { grid-template-columns: 1fr; } + .todo-list { padding: 0 .75rem 1.5rem; } + .todo-main-header { padding: .75rem 1rem; } + .todo-quick-add { margin: .75rem 1rem .5rem; } +} + +/* Dark mode */ +[data-theme="dark"] .todo-sidebar,.todo-sidebar-top { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-nav-item:hover { background: var(--bg-page); } +[data-theme="dark"] .todo-nav-item.active { background: rgba(59,130,246,.15); } +[data-theme="dark"] .todo-main { background: var(--bg-page); } +[data-theme="dark"] .todo-main-header { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-quick-add { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-quick-add input { color: var(--text-main); } +[data-theme="dark"] .todo-item { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-item:hover { border-color: #484f58; } +[data-theme="dark"] .todo-title { color: var(--text-main); } +[data-theme="dark"] .todo-check { border-color: #484f58; } +[data-theme="dark"] .todo-check:hover { background: rgba(59,130,246,.15); border-color: var(--primary); } +[data-theme="dark"] .todo-modal { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-modal-header,.todo-modal-footer { border-color: var(--border-light); } +[data-theme="dark"] .form-control { background: #1c2128; border-color: var(--border-light); color: var(--text-main); } +[data-theme="dark"] .btn-secondary { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .btn-secondary:hover { background: rgba(255,255,255,.06); } +[data-theme="dark"] .btn-danger { background: var(--bg-panel); } +[data-theme="dark"] .todo-stats { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .todo-toast { background: var(--bg-panel); border-color: var(--border-light); color: var(--text-main); } diff --git a/modules/todo/assets/todo.js b/modules/todo/assets/todo.js new file mode 100644 index 0000000..9a652ff --- /dev/null +++ b/modules/todo/assets/todo.js @@ -0,0 +1,331 @@ +// HouseHub — Todo module +const API = '/modules/todo/api.php'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── +function escHtml(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML;} +function fmtDate(d){if(!d)return null;const dt=new Date(d+'T00:00:00');return dt.toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit'});} +function isToday(d){if(!d)return false;return d===new Date().toISOString().slice(0,10);} +function isOverdue(d){if(!d)return false;return dt.remove(),3000); +} +async function api(action,method='GET',data=null,extra=''){ + const opts={method,headers:{}}; + if(data){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(data);} + const r=await fetch(API+'?action='+action+extra,opts); + const j=await r.json(); + if(!j.ok)throw new Error(j.error||'Erreur'); + return j.data; +} + +// ─── State ─────────────────────────────────────────────────────────────────── +let currentFilter='all'; +let showDone=false; +let lists=[]; +let editTodoId=null; +let editListId=null; +let selectedPriority='none'; +let selectedColor='#3b82f6'; + +const COLORS=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#84cc16','#f97316','#64748b']; +const ICONS=['📋','🏠','🛒','💼','💪','🎯','📚','✈️','🎮','❤️','⭐','🔧']; + +// ─── Sidebar ───────────────────────────────────────────────────────────────── +async function loadSidebar(){ + try{ + const[data,stats]=await Promise.all([api('lists'),api('stats')]); + lists=data; + const el=document.getElementById('todo-sidebar-lists'); + + // Smart views + let smartHtml=` + Vue + + 📋 Toutes ${stats.pending} + + 📅 Aujourd'hui ${stats.today} + + 🗓️ À venir`; + if(parseInt(stats.overdue)>0){ + smartHtml+=` + 🔴 En retard ${stats.overdue}`; + } + smartHtml+=` + ✅ Terminées ${stats.done}`; + + // Lists + let listsHtml='Listes +'; + lists.forEach(l=>{ + const act=currentFilter==='list_'+l.id?' active':''; + listsHtml+=` + + ${escHtml(l.icon)} ${escHtml(l.name)} + ${l.pending||0} + `; + }); + + el.innerHTML=smartHtml+listsHtml; + }catch(e){} +} + +function setFilter(f){ + currentFilter=f;showDone=f==='done'; + loadSidebar();loadTodos(); +} + +// ─── Todo list ──────────────────────────────────────────────────────────────── +async function loadTodos(){ + try{ + let extra=''; + if(currentFilter==='all'){extra='';} + else if(currentFilter==='done'){extra='&show_done=1';} + else if(currentFilter==='today'){extra='&list_id=today';} + else if(currentFilter==='upcoming'){extra='&list_id=upcoming';} + else if(currentFilter==='overdue'){extra='&list_id=overdue';} + else if(currentFilter.startsWith('list_')){extra='&list_id='+currentFilter.slice(5);} + if(showDone&¤tFilter!=='done')extra+='&show_done=1'; + + const todos=await api('todos','GET',null,extra); + renderTodos(todos); + // Update quick-add list selector + updateQuickAddList(); + // Header title + updateHeader(); + }catch(e){toast(e.message,'error');} +} + +function updateHeader(){ + const el=document.getElementById('todo-header-title'); + if(!el)return; + if(currentFilter==='all')el.textContent='Toutes les tâches'; + else if(currentFilter==='today')el.textContent='Aujourd\'hui'; + else if(currentFilter==='upcoming')el.textContent='À venir'; + else if(currentFilter==='done')el.textContent='Tâches terminées'; + else if(currentFilter==='overdue')el.textContent='En retard'; + else if(currentFilter.startsWith('list_')){ + const l=lists.find(x=>x.id==currentFilter.slice(5)); + el.textContent=l?(l.icon+' '+l.name):'Liste'; + } +} + +function updateQuickAddList(){ + const sel=document.getElementById('quick-add-list'); + if(!sel)return; + sel.innerHTML='Sans liste'+ + lists.map(l=>`${escHtml(l.icon)} ${escHtml(l.name)}`).join(''); + // Pre-select current list + if(currentFilter.startsWith('list_')) sel.value=currentFilter.slice(5); +} + +function renderTodos(todos){ + const el=document.getElementById('todo-list'); + if(!todos.length){ + el.innerHTML=`${currentFilter==='done'?'✅':'📋'} + ${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche. Ajoutez-en une !'}`; + return; + } + // Group: pending then done + const pending=todos.filter(t=>!t.done); + const done=todos.filter(t=>t.done); + let html=''; + pending.forEach(t=>{ html+=todoItemHtml(t); }); + if(done.length&&showDone){ + html+=` + Terminées (${done.length}) Masquer ✕`; + done.forEach(t=>{ html+=todoItemHtml(t); }); + } else if(done.length&&!showDone){ + html+=` + ✅ Afficher ${done.length} tâche${done.length>1?'s':''} terminée${done.length>1?'s':''}`; + } + el.innerHTML=html; +} + +function todoItemHtml(t){ + const checkClass='todo-check'+(t.done?' checked':''); + const itemClass='todo-item'+(t.done?' done':''); + const dueCls=isOverdue(t.due_date)&&!t.done?' overdue':isToday(t.due_date)&&!t.done?' today':''; + const dueLabel=t.due_date?(isToday(t.due_date)?'Aujourd\'hui':isOverdue(t.due_date)?'En retard '+fmtDate(t.due_date):fmtDate(t.due_date)):null; + const priBadge=t.priority&&t.priority!=='none'? + `${t.priority==='high'?'Urgent':t.priority==='medium'?'Normal':'Bas'}`:''; + const listBadge=t.list_name? + `${escHtml(t.list_icon||'')} ${escHtml(t.list_name)}`:''; + return ` + + + ${escHtml(t.title)} + ${t.notes?`${escHtml(t.notes)}`:''} + + ${dueLabel?`📅 ${escHtml(dueLabel)}`:''} + ${priBadge}${listBadge} + + + + ✏️ + 🗑️ + + `; +} + +async function toggleDone(e,id,done){ + e.stopPropagation(); + try{ + await api('todos','PUT',{done:done===1},'&id='+id); + const item=document.querySelector(`.todo-item[data-id="${id}"]`); + if(item){ + const check=item.querySelector('.todo-check'); + check.classList.add('just-done'); + setTimeout(()=>{check.classList.remove('just-done');loadTodos();loadSidebar();},300); + } + }catch(e){toast(e.message,'error');} +} + +async function deleteTodo(e,id){ + e.stopPropagation(); + if(!confirm('Supprimer cette tâche ?'))return; + try{await api('todos','DELETE',null,'&id='+id);toast('Supprimée');loadTodos();loadSidebar();} + catch(e){toast(e.message,'error');} +} + +// ─── Quick add ──────────────────────────────────────────────────────────────── +async function quickAdd(e){ + if(e.key!=='Enter')return; + const inp=document.getElementById('quick-add-input'); + const title=inp.value.trim(); + if(!title)return; + const listSel=document.getElementById('quick-add-list'); + try{ + await api('todos','POST',{title,list_id:listSel?.value||null,priority:'none'}); + inp.value='';toast('Tâche ajoutée');loadTodos();loadSidebar(); + }catch(e){toast(e.message,'error');} +} + +// ─── Todo modal ─────────────────────────────────────────────────────────────── +function openAddTodo(){ + editTodoId=null;selectedPriority='none'; + document.getElementById('todo-modal-title').textContent='Nouvelle tâche'; + document.getElementById('todo-form-title').value=''; + document.getElementById('todo-form-notes').value=''; + document.getElementById('todo-form-due').value=''; + document.getElementById('todo-delete-btn').style.display='none'; + // List selector + const sel=document.getElementById('todo-form-list'); + sel.innerHTML='Sans liste'+lists.map(l=>`${escHtml(l.icon)} ${escHtml(l.name)}`).join(''); + if(currentFilter.startsWith('list_')) sel.value=currentFilter.slice(5); + setPriority('none'); + openModal('todo-modal'); +} + +async function openEditTodo(id){ + editTodoId=id; + try{ + const todos=await api('todos','GET',null,'&action=todos'); + // fetch single - use filter by all and find + const all=await api('todos','GET',null,'&show_done=1'); + const t=all.find(x=>x.id==id); + if(!t)return; + document.getElementById('todo-modal-title').textContent='Modifier la tâche'; + document.getElementById('todo-form-title').value=t.title; + document.getElementById('todo-form-notes').value=t.notes||''; + document.getElementById('todo-form-due').value=t.due_date||''; + document.getElementById('todo-delete-btn').style.display=''; + const sel=document.getElementById('todo-form-list'); + sel.innerHTML='Sans liste'+lists.map(l=>`${escHtml(l.icon)} ${escHtml(l.name)}`).join(''); + sel.value=t.list_id||''; + setPriority(t.priority||'none'); + openModal('todo-modal'); + }catch(e){toast(e.message,'error');} +} + +function setPriority(p){ + selectedPriority=p; + document.querySelectorAll('.priority-opt').forEach(el=>{ + el.className='priority-opt'; + if(el.dataset.p===p) el.classList.add('sel-'+p); + }); +} + +async function saveTodo(){ + const title=document.getElementById('todo-form-title').value.trim(); + if(!title){toast('Titre requis','error');return;} + const data={ + title, + notes:document.getElementById('todo-form-notes').value||null, + due_date:document.getElementById('todo-form-due').value||null, + list_id:document.getElementById('todo-form-list').value||null, + priority:selectedPriority + }; + try{ + if(editTodoId){await api('todos','PUT',data,'&id='+editTodoId);toast('Tâche mise à jour');} + else{await api('todos','POST',data);toast('Tâche ajoutée');} + closeModal('todo-modal');loadTodos();loadSidebar(); + }catch(e){toast(e.message,'error');} +} + +async function deleteTodoFromModal(){ + if(!editTodoId||!confirm('Supprimer cette tâche ?'))return; + try{await api('todos','DELETE',null,'&id='+editTodoId);closeModal('todo-modal');toast('Supprimée');loadTodos();loadSidebar();} + catch(e){toast(e.message,'error');} +} + +// ─── List modal ─────────────────────────────────────────────────────────────── +function openListModal(id=null){ + editListId=id;selectedColor='#3b82f6'; + let list=id?lists.find(l=>l.id==id):null; + document.getElementById('list-modal-title').textContent=id?'Modifier la liste':'Nouvelle liste'; + document.getElementById('list-form-name').value=list?.name||''; + document.getElementById('list-delete-btn').style.display=id?'':'none'; + selectedColor=list?.color||'#3b82f6'; + // Render swatches + document.getElementById('list-color-swatches').innerHTML=COLORS.map(c=> + ``).join(''); + // Render icons + document.getElementById('list-icon-opts').innerHTML=ICONS.map(i=> + `${i}`).join(''); + openModal('list-modal'); +} +function selectColor(c){ + selectedColor=c; + document.querySelectorAll('.color-swatch').forEach(s=>{s.classList.toggle('selected',s.style.background===c||rgbToHex(s.style.background)===c);}); +} +function rgbToHex(rgb){ + const m=rgb.match(/\d+/g);if(!m||m.length<3)return rgb; + return '#'+m.slice(0,3).map(x=>parseInt(x).toString(16).padStart(2,'0')).join(''); +} +let selectedIcon='📋'; +function selectIcon(btn,icon){ + selectedIcon=icon; + document.querySelectorAll('#list-icon-opts button').forEach(b=>{b.style.background='';b.style.borderColor='';}); + btn.style.background='#eff6ff';btn.style.borderColor='var(--primary)'; +} +async function saveList(){ + const name=document.getElementById('list-form-name').value.trim(); + if(!name){toast('Nom requis','error');return;} + const data={name,color:selectedColor,icon:selectedIcon}; + try{ + if(editListId){await api('lists','PUT',data,'&id='+editListId);} + else{await api('lists','POST',data);} + closeModal('list-modal');toast(editListId?'Liste mise à jour':'Liste créée'); + loadSidebar(); + }catch(e){toast(e.message,'error');} +} +async function deleteList(){ + if(!editListId||!confirm('Supprimer la liste et toutes ses tâches ?'))return; + try{await api('lists','DELETE',null,'&id='+editListId);closeModal('list-modal');toast('Liste supprimée');setFilter('all');} + catch(e){toast(e.message,'error');} +} + +// ─── Modal helpers ──────────────────────────────────────────────────────────── +function openModal(id){document.getElementById(id)?.classList.add('show');} +function closeModal(id){document.getElementById(id)?.classList.remove('show');} + +// ─── Init ───────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded',()=>{ + loadSidebar(); + loadTodos(); + // Quick add enter + document.getElementById('quick-add-input')?.addEventListener('keydown',quickAdd); + // Modal backdrops + document.querySelectorAll('.todo-modal-backdrop').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('show');})); +}); diff --git a/settings.php b/settings.php index 875c397..79ceb3a 100644 --- a/settings.php +++ b/settings.php @@ -15,7 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { - $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo']; + $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); if (empty($enabled)) { $error = "Vous devez garder au moins un module actif."; @@ -188,6 +188,7 @@ require __DIR__ . '/header.php'; 'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')], 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], + 'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], ]; ?> diff --git a/todo.php b/todo.php new file mode 100644 index 0000000..df2178c --- /dev/null +++ b/todo.php @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + Toutes les tâches + + + + + 👁 Afficher terminées + + + Tâche + + + + + + + + + + + + Options + + + + + + + + + + + + + Nouvelle tâche + × + + + + + + Titre * + + + + + Notes + + + + + + Date limite + + + + Liste + + — Aucune — + + + + + + Priorité + + — Aucune + 🟢 Basse + 🟡 Moyenne + 🔴 Haute + + + + + + + + + + + + + Nouvelle liste + × + + + + + + Nom * + + + + + Icône + + + + + Couleur + + + + + + + + + +
${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche. Ajoutez-en une !'}