Fix Todo module: ID mismatches, overdue filter, Discord webhook settings
Deploy HouseHub / deploy (push) Successful in 2s

- Rewrite todo.php with correct element IDs matching todo.js
- Fix sidebar: fully dynamic rendering (no more static nav items)
- Fix quick-add: list selector added, Enter key works correctly
- Fix modals: todo-form-*, list-form-name, list-icon-opts IDs
- Fix overdue filter missing from API (list_id=overdue)
- Add settings modal (⚙️) with Discord webhook URL field
- Add settings action in API (GET/PUT via pf_notes table)
- Discord webhook: notify on task create and task done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-05-12 14:46:32 +02:00
co-authored by Claude Sonnet 4.6
parent b97cba6e75
commit 7790375c01
3 changed files with 187 additions and 124 deletions
+44 -4
View File
@@ -19,6 +19,17 @@ function tOk($d) { echo json_encode(['ok' => true, 'data' => $d], JSON_UNESCAPE
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) ?? []; }
function discordNotify(PDO $pdo, string $msg): void {
$s = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='todo_settings' AND reference_id='webhook_discord'");
$s->execute(); $url = $s->fetchColumn();
if (!$url) return;
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>4,
CURLOPT_HTTPHEADER=>['Content-Type: application/json'],
CURLOPT_POSTFIELDS=>json_encode(['username'=>'HouseHub Todo','content'=>$msg])]);
curl_exec($ch); curl_close($ch);
}
// ── LISTS ──────────────────────────────────────────────────────────────────────
if ($action === 'lists') {
if ($method === 'GET') {
@@ -71,14 +82,17 @@ if ($action === 'todos') {
} elseif ($list_id === 'upcoming') {
$where[] = 't.due_date >= CURDATE()';
$where[] = 't.done = 0';
} elseif ($list_id === 'overdue') {
$where[] = 't.due_date < CURDATE()';
$where[] = 't.done = 0';
} elseif ($list_id === 'done') {
$where[] = 't.done = 1';
} else {
$where[] = 't.list_id = ?'; $params[] = $list_id;
}
if (!$show_done && $list_id !== 'done') {
if (!$show_done && !in_array($list_id, ['done', 'overdue'])) {
$where[] = 't.done = 0';
} elseif ($list_id === 'done') {
$where[] = 't.done = 1';
}
if ($priority) { $where[] = 't.priority = ?'; $params[] = $priority; }
@@ -105,7 +119,10 @@ if ($action === 'todos') {
]);
$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());
$new->execute([$id]);
$row = $new->fetch();
discordNotify($pdo, "📋 **Nouvelle tâche** : " . $title . ($row['list_name'] ? " _(". $row['list_name'] .")_" : ""));
tOk($row);
}
if ($method === 'PUT') {
@@ -117,6 +134,11 @@ if ($action === 'todos') {
$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]);
if ($done) {
$t = $pdo->prepare("SELECT title FROM pf_todos WHERE id=?"); $t->execute([$id]);
$row = $t->fetch();
if ($row) discordNotify($pdo, "✅ **Tâche terminée** : " . $row['title']);
}
tOk(['done' => $done]);
}
@@ -145,4 +167,22 @@ if ($action === 'stats') {
]);
}
// ── SETTINGS ───────────────────────────────────────────────────────────────────
if ($action === 'settings') {
if ($method === 'GET') {
$rows = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type='todo_settings'")->fetchAll();
$s = [];
foreach ($rows as $r) $s[$r['reference_id']] = $r['content'];
tOk($s);
}
if ($method === 'PUT') {
$d = tBody();
foreach ($d as $key => $val) {
$pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('todo_settings',?,?) ON DUPLICATE KEY UPDATE content=?")
->execute([$key, $val ?? '', $val ?? '']);
}
tOk(['updated' => true]);
}
}
tErr('Action inconnue', 404);
+98 -64
View File
@@ -28,6 +28,7 @@ let editTodoId=null;
let editListId=null;
let selectedPriority='none';
let selectedColor='#3b82f6';
let selectedIcon='📋';
const COLORS=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#84cc16','#f97316','#64748b'];
const ICONS=['📋','🏠','🛒','💼','💪','🎯','📚','✈️','🎮','❤️','⭐','🔧'];
@@ -39,35 +40,45 @@ async function loadSidebar(){
lists=data;
const el=document.getElementById('todo-sidebar-lists');
// Smart views
let smartHtml=`
<div class="todo-nav-section">Vue</div>
let html=`
<div class="todo-nav-section">Vues</div>
<div class="todo-nav-item${currentFilter==='all'?' active':''}" onclick="setFilter('all')">
📋 Toutes <span class="todo-nav-badge">${stats.pending}</span></div>
<span>📋</span> Toutes
<span class="todo-nav-badge">${stats.pending||0}</span>
</div>
<div class="todo-nav-item${currentFilter==='today'?' active':''}" onclick="setFilter('today')">
📅 Aujourd'hui <span class="todo-nav-badge${parseInt(stats.today)>0?' urgent':''}">${stats.today}</span></div>
<span>☀️</span> Aujourd'hui
<span class="todo-nav-badge${parseInt(stats.today)>0?' urgent':''}">${stats.today||0}</span>
</div>
<div class="todo-nav-item${currentFilter==='upcoming'?' active':''}" onclick="setFilter('upcoming')">
🗓️ À venir</div>`;
<span>📅</span> À venir
</div>`;
if(parseInt(stats.overdue)>0){
smartHtml+=`<div class="todo-nav-item${currentFilter==='overdue'?' active':''}" onclick="setFilter('overdue')">
🔴 En retard <span class="todo-nav-badge urgent">${stats.overdue}</span></div>`;
html+=`<div class="todo-nav-item${currentFilter==='overdue'?' active':''}" onclick="setFilter('overdue')">
<span>⚠️</span> En retard
<span class="todo-nav-badge urgent">${stats.overdue}</span>
</div>`;
}
smartHtml+=`<div class="todo-nav-item${currentFilter==='done'?' active':''}" onclick="setFilter('done')">
✅ Terminées <span class="todo-nav-badge">${stats.done}</span></div>`;
html+=`<div class="todo-nav-item${currentFilter==='done'?' active':''}" onclick="setFilter('done')">
<span>✅</span> Terminées
<span class="todo-nav-badge">${stats.done||0}</span>
</div>`;
// Lists
let listsHtml='<div class="todo-nav-section" style="display:flex;align-items:center;justify-content:space-between;padding-right:.75rem">Listes <button class="btn-ghost btn-sm" onclick="openListModal()" title="Nouvelle liste" style="font-size:.9rem">+</button></div>';
html+=`<div class="todo-nav-section" style="display:flex;align-items:center;justify-content:space-between;padding-right:.75rem">
Listes <button class="btn btn-ghost btn-icon btn-sm" onclick="openListModal()" title="Nouvelle liste" style="padding:.1rem .3rem;font-size:.85rem">+</button>
</div>`;
lists.forEach(l=>{
const act=currentFilter==='list_'+l.id?' active':'';
listsHtml+=`<div class="todo-nav-item${act}" onclick="setFilter('list_${l.id}')">
html+=`<div class="todo-nav-item${act}" onclick="setFilter('list_${l.id}')">
<span class="todo-list-dot" style="background:${escHtml(l.color)}"></span>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(l.icon)} ${escHtml(l.name)}</span>
<span class="todo-nav-badge">${l.pending||0}</span>
<button class="btn btn-ghost btn-icon" onclick="event.stopPropagation();openListModal(${l.id})" title="Modifier" style="opacity:.5;padding:.1rem .2rem;font-size:.8rem">✏️</button>
</div>`;
});
el.innerHTML=smartHtml+listsHtml;
}catch(e){}
el.innerHTML=html;
}catch(e){console.error(e);}
}
function setFilter(f){
@@ -79,19 +90,17 @@ function setFilter(f){
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(currentFilter==='all')extra='';
else if(currentFilter==='done')extra='&list_id=done&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&&currentFilter!=='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');}
}
@@ -99,12 +108,9 @@ async function loadTodos(){
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 map={all:'Toutes les tâches',today:"Aujourd'hui",upcoming:'À venir',done:'Tâches terminées',overdue:'En retard'};
if(map[currentFilter]){el.textContent=map[currentFilter];return;}
if(currentFilter.startsWith('list_')){
const l=lists.find(x=>x.id==currentFilter.slice(5));
el.textContent=l?(l.icon+' '+l.name):'Liste';
}
@@ -115,27 +121,31 @@ function updateQuickAddList(){
if(!sel)return;
sel.innerHTML='<option value="">Sans liste</option>'+
lists.map(l=>`<option value="${l.id}">${escHtml(l.icon)} ${escHtml(l.name)}</option>`).join('');
// Pre-select current list
if(currentFilter.startsWith('list_')) sel.value=currentFilter.slice(5);
if(currentFilter.startsWith('list_'))sel.value=currentFilter.slice(5);
}
function toggleShowDone(){
showDone=!showDone;
const btn=document.getElementById('show-done-btn');
if(btn)btn.innerHTML=showDone?'🙈 Masquer terminées':'👁 Afficher terminées';
loadTodos();
}
function renderTodos(todos){
const el=document.getElementById('todo-list');
if(!todos.length){
el.innerHTML=`<div class="todo-empty"><div class="icon">${currentFilter==='done'?'✅':'📋'}</div>
<p>${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche. Ajoutez-en une !'}</p></div>`;
<p>${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche pour le moment.'}</p></div>`;
return;
}
// Group: pending then done
const pending=todos.filter(t=>!t.done);
const done=todos.filter(t=>t.done);
const pending=todos.filter(t=>!parseInt(t.done));
const done=todos.filter(t=>parseInt(t.done));
let html='';
pending.forEach(t=>{ html+=todoItemHtml(t); });
pending.forEach(t=>{html+=todoItemHtml(t);});
if(done.length&&showDone){
html+=`<div class="todo-group-label" style="display:flex;align-items:center;justify-content:space-between">
Terminées (${done.length}) <button class="show-done-btn" onclick="showDone=false;loadTodos()">Masquer ✕</button></div>`;
done.forEach(t=>{ html+=todoItemHtml(t); });
} else if(done.length&&!showDone){
html+=`<div class="todo-group-label">Terminées (${done.length})</div>`;
done.forEach(t=>{html+=todoItemHtml(t);});
}else if(done.length&&!showDone&&currentFilter!=='done'){
html+=`<div style="padding:.5rem 0"><button class="show-done-btn" onclick="showDone=true;loadTodos()">
✅ Afficher ${done.length} tâche${done.length>1?'s':''} terminée${done.length>1?'s':''}</button></div>`;
}
@@ -143,16 +153,17 @@ function renderTodos(todos){
}
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 isDone=parseInt(t.done);
const checkClass='todo-check'+(isDone?' checked':'');
const itemClass='todo-item'+(isDone?' done':'');
const dueCls=isOverdue(t.due_date)&&!isDone?' overdue':isToday(t.due_date)&&!isDone?' 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'?
`<span class="todo-priority-badge pri-${t.priority}">${t.priority==='high'?'Urgent':t.priority==='medium'?'Normal':'Bas'}</span>`:'';
const listBadge=t.list_name?
`<span class="todo-list-badge" style="background:${escHtml(t.list_color||'#3b82f6')}22;color:${escHtml(t.list_color||'#3b82f6')}">${escHtml(t.list_icon||'')} ${escHtml(t.list_name)}</span>`:'';
return `<div class="${itemClass}" data-id="${t.id}" data-priority="${t.priority||'none'}">
<div class="${checkClass}" onclick="toggleDone(event,${t.id},${t.done?0:1})"></div>
return `<div class="${itemClass}" data-id="${t.id}" data-priority="${escHtml(t.priority||'none')}">
<div class="${checkClass}" onclick="toggleDone(event,${t.id},${isDone?0:1})"></div>
<div class="todo-content" onclick="openEditTodo(${t.id})">
<div class="todo-title">${escHtml(t.title)}</div>
${t.notes?`<div class="todo-notes-preview">${escHtml(t.notes)}</div>`:''}
@@ -197,31 +208,29 @@ async function quickAdd(e){
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();
inp.value='';toast('Tâche ajoutée');loadTodos();loadSidebar();
}catch(e){toast(e.message,'error');}
}
// ─── Todo modal ───────────────────────────────────────────────────────────────
function openAddTodo(){
editTodoId=null;selectedPriority='none';
editTodoId=null;
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='<option value="">Sans liste</option>'+lists.map(l=>`<option value="${l.id}">${escHtml(l.icon)} ${escHtml(l.name)}</option>`).join('');
if(currentFilter.startsWith('list_')) sel.value=currentFilter.slice(5);
sel.innerHTML='<option value="">— Aucune —</option>'+lists.map(l=>`<option value="${l.id}">${escHtml(l.icon)} ${escHtml(l.name)}</option>`).join('');
if(currentFilter.startsWith('list_'))sel.value=currentFilter.slice(5);
setPriority('none');
openModal('todo-modal');
setTimeout(()=>document.getElementById('todo-form-title').focus(),50);
}
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;
@@ -231,7 +240,7 @@ async function openEditTodo(id){
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='<option value="">Sans liste</option>'+lists.map(l=>`<option value="${l.id}">${escHtml(l.icon)} ${escHtml(l.name)}</option>`).join('');
sel.innerHTML='<option value="">— Aucune —</option>'+lists.map(l=>`<option value="${l.id}">${escHtml(l.icon)} ${escHtml(l.name)}</option>`).join('');
sel.value=t.list_id||'';
setPriority(t.priority||'none');
openModal('todo-modal');
@@ -242,7 +251,7 @@ 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);
if(el.dataset.p===p)el.classList.add('sel-'+p);
});
}
@@ -258,7 +267,7 @@ async function saveTodo(){
};
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');}
else{await api('todos','POST',data);toast('Tâche ajoutée');}
closeModal('todo-modal');loadTodos();loadSidebar();
}catch(e){toast(e.message,'error');}
}
@@ -271,34 +280,43 @@ async function deleteTodoFromModal(){
// ─── List modal ───────────────────────────────────────────────────────────────
function openListModal(id=null){
editListId=id;selectedColor='#3b82f6';
editListId=id;
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
selectedIcon=list?.icon||'📋';
document.getElementById('list-color-swatches').innerHTML=COLORS.map(c=>
`<div class="color-swatch${c===selectedColor?' selected':''}" style="background:${c}" onclick="selectColor('${c}')"></div>`).join('');
// Render icons
document.getElementById('list-icon-opts').innerHTML=ICONS.map(i=>
`<button type="button" class="btn btn-sm btn-secondary" onclick="selectIcon(this,'${i}')" style="${list?.icon===i?'background:#eff6ff;border-color:var(--primary)':''}">${i}</button>`).join('');
`<button type="button" class="btn btn-sm btn-secondary${i===selectedIcon?' ':''}" onclick="selectIcon(this,'${i}')"
style="${i===selectedIcon?'background:#eff6ff;border-color:var(--primary)':''}">${i}</button>`).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);});
document.querySelectorAll('.color-swatch').forEach(s=>{
const hex=rgbToHex(s.style.background);
s.classList.toggle('selected',hex===c||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;}
@@ -306,16 +324,34 @@ async function saveList(){
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');
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');}
}
// ─── Settings modal ───────────────────────────────────────────────────────────
async function openSettings(){
try{
const s=await api('settings');
document.getElementById('settings-webhook').value=s.webhook_discord||'';
openModal('settings-modal');
}catch(e){openModal('settings-modal');}
}
async function saveSettings(){
const webhook=document.getElementById('settings-webhook').value.trim();
try{
await api('settings','PUT',{webhook_discord:webhook});
toast('Paramètres enregistrés ✓');closeModal('settings-modal');
}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');}
@@ -324,8 +360,6 @@ function closeModal(id){document.getElementById(id)?.classList.remove('show');}
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');}));
});
+45 -56
View File
@@ -14,40 +14,12 @@ require __DIR__ . '/header.php';
<div class="todo-layout">
<!-- ── SIDEBAR ────────────────────────────────────────────────────────────── -->
<aside class="todo-sidebar" id="todo-sidebar">
<aside class="todo-sidebar">
<div class="todo-sidebar-top">
<button class="btn btn-primary" onclick="openAddTodo()" style="width:100%;justify-content:center">+ Nouvelle tâche</button>
</div>
<!-- Smart views -->
<div class="todo-nav-section">Vues</div>
<a class="todo-nav-item active" data-filter="all" onclick="setFilter('all')">
<span>📋</span> Toutes
<span class="todo-nav-badge" id="badge-all">0</span>
</a>
<a class="todo-nav-item" data-filter="today" onclick="setFilter('today')">
<span>☀️</span> Aujourd'hui
<span class="todo-nav-badge urgent" id="badge-today">0</span>
</a>
<a class="todo-nav-item" data-filter="upcoming" onclick="setFilter('upcoming')">
<span>📅</span> À venir
<span class="todo-nav-badge" id="badge-upcoming">0</span>
</a>
<a class="todo-nav-item" data-filter="overdue" onclick="setFilter('overdue')">
<span>⚠️</span> En retard
<span class="todo-nav-badge urgent" id="badge-overdue">0</span>
</a>
<a class="todo-nav-item" data-filter="done" onclick="setFilter('done')">
<span>✅</span> Terminées
<span class="todo-nav-badge" id="badge-done">0</span>
</a>
<!-- User lists -->
<div class="todo-nav-section" style="display:flex;align-items:center;justify-content:space-between;padding-right:.75rem">
Listes
<button class="btn btn-ghost btn-icon btn-sm" onclick="openListModal()" title="Nouvelle liste" style="padding:.1rem .3rem;font-size:.85rem">+</button>
</div>
<div id="todo-lists-nav"></div>
<!-- Contenu rendu dynamiquement par JS (vues + listes) -->
<div id="todo-sidebar-lists"></div>
</aside>
<!-- ── MAIN ───────────────────────────────────────────────────────────────── -->
@@ -56,25 +28,23 @@ require __DIR__ . '/header.php';
<!-- Header -->
<div class="todo-main-header">
<div>
<h2 id="todo-view-title">Toutes les tâches</h2>
<h2 id="todo-header-title">Toutes les tâches</h2>
<div class="todo-subtitle" id="todo-view-subtitle"></div>
</div>
<div class="todo-header-actions">
<button class="show-done-btn" id="show-done-btn" onclick="toggleShowDone()">
<span id="show-done-icon">👁</span> <span id="show-done-label">Afficher terminées</span>
👁 Afficher terminées
</button>
<button class="btn btn-ghost btn-icon" onclick="openSettings()" title="Paramètres Todo">⚙️</button>
<button class="btn btn-primary btn-sm" onclick="openAddTodo()">+ Tâche</button>
</div>
</div>
<!-- Stats bar -->
<div class="todo-stats" id="todo-stats-bar"></div>
<!-- Quick add -->
<div class="todo-quick-add">
<span style="color:var(--text-muted)">+</span>
<input type="text" id="quick-add-input" placeholder="Ajouter une tâche rapide… (Entrée pour valider)">
<button class="btn btn-ghost btn-sm" id="quick-add-list-btn" onclick="openAddTodo(document.getElementById('quick-add-input').value)">Options</button>
<select id="quick-add-list" class="form-control" style="width:auto;font-size:.8rem;padding:.25rem .5rem;border:none;background:transparent;color:var(--text-muted)"></select>
</div>
<!-- Todo list -->
@@ -88,29 +58,28 @@ require __DIR__ . '/header.php';
<div class="todo-modal">
<div class="todo-modal-header">
<h3 id="todo-modal-title">Nouvelle tâche</h3>
<button class="todo-modal-close" onclick="closeTodoModal()">×</button>
<button class="todo-modal-close" onclick="closeModal('todo-modal')">×</button>
</div>
<div class="todo-modal-body">
<input type="hidden" id="todo-id">
<div class="form-group">
<label class="form-label">Titre *</label>
<input type="text" id="todo-title" class="form-control" placeholder="Ex : Appeler le plombier" autofocus>
<input type="text" id="todo-form-title" class="form-control" placeholder="Ex : Appeler le plombier">
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea id="todo-notes" class="form-control" placeholder="Détails, liens, infos…"></textarea>
<textarea id="todo-form-notes" class="form-control" placeholder="Détails, liens, infos…"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Date limite</label>
<input type="date" id="todo-due" class="form-control">
<input type="date" id="todo-form-due" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Liste</label>
<select id="todo-list-select" class="form-control">
<select id="todo-form-list" class="form-control">
<option value="">— Aucune —</option>
</select>
</div>
@@ -119,17 +88,16 @@ require __DIR__ . '/header.php';
<div class="form-group">
<label class="form-label">Priorité</label>
<div class="priority-opts">
<div class="priority-opt sel-none" data-p="none" onclick="selectPriority('none')">— Aucune</div>
<div class="priority-opt" data-p="low" onclick="selectPriority('low')">🟢 Basse</div>
<div class="priority-opt" data-p="medium" onclick="selectPriority('medium')">🟡 Moyenne</div>
<div class="priority-opt" data-p="high" onclick="selectPriority('high')">🔴 Haute</div>
<div class="priority-opt sel-none" data-p="none" onclick="setPriority('none')">— Aucune</div>
<div class="priority-opt" data-p="low" onclick="setPriority('low')">🟢 Basse</div>
<div class="priority-opt" data-p="medium" onclick="setPriority('medium')">🟡 Moyenne</div>
<div class="priority-opt" data-p="high" onclick="setPriority('high')">🔴 Haute</div>
</div>
<input type="hidden" id="todo-priority" value="none">
</div>
</div>
<div class="todo-modal-footer">
<button class="btn btn-danger btn-sm" id="todo-delete-btn" onclick="deleteTodo()" style="display:none;margin-right:auto">Supprimer</button>
<button class="btn btn-secondary" onclick="closeTodoModal()">Annuler</button>
<button class="btn btn-danger btn-sm" id="todo-delete-btn" onclick="deleteTodoFromModal()" style="display:none;margin-right:auto">Supprimer</button>
<button class="btn btn-secondary" onclick="closeModal('todo-modal')">Annuler</button>
<button class="btn btn-primary" onclick="saveTodo()">Enregistrer</button>
</div>
</div>
@@ -140,34 +108,55 @@ require __DIR__ . '/header.php';
<div class="todo-modal">
<div class="todo-modal-header">
<h3 id="list-modal-title">Nouvelle liste</h3>
<button class="todo-modal-close" onclick="closeListModal()">×</button>
<button class="todo-modal-close" onclick="closeModal('list-modal')">×</button>
</div>
<div class="todo-modal-body">
<input type="hidden" id="list-id">
<div class="form-group">
<label class="form-label">Nom *</label>
<input type="text" id="list-name" class="form-control" placeholder="Ex : Maison, Travail…">
<input type="text" id="list-form-name" class="form-control" placeholder="Ex : Maison, Travail…">
</div>
<div class="form-group">
<label class="form-label">Icône</label>
<input type="text" id="list-icon" class="form-control" placeholder="📋" maxlength="4" style="font-size:1.2rem;width:80px">
<div id="list-icon-opts" style="display:flex;flex-wrap:wrap;gap:.35rem"></div>
</div>
<div class="form-group">
<label class="form-label">Couleur</label>
<div class="color-swatches" id="list-color-swatches"></div>
<input type="hidden" id="list-color" value="#3b82f6">
</div>
</div>
<div class="todo-modal-footer">
<button class="btn btn-danger btn-sm" id="list-delete-btn" onclick="deleteList()" style="display:none;margin-right:auto">Supprimer</button>
<button class="btn btn-secondary" onclick="closeListModal()">Annuler</button>
<button class="btn btn-secondary" onclick="closeModal('list-modal')">Annuler</button>
<button class="btn btn-primary" onclick="saveList()">Enregistrer</button>
</div>
</div>
</div>
<!-- ── MODAL : PARAMÈTRES ─────────────────────────────────────────────────────── -->
<div class="todo-modal-backdrop" id="settings-modal">
<div class="todo-modal">
<div class="todo-modal-header">
<h3>⚙️ Paramètres Todo</h3>
<button class="todo-modal-close" onclick="closeModal('settings-modal')">×</button>
</div>
<div class="todo-modal-body">
<div class="form-group">
<label class="form-label">Webhook Discord</label>
<input type="url" id="settings-webhook" class="form-control" placeholder="https://discord.com/api/webhooks/…">
<div style="font-size:.75rem;color:var(--text-muted);margin-top:.25rem">
Recevoir une notification Discord quand une tâche est créée ou terminée.
</div>
</div>
</div>
<div class="todo-modal-footer">
<button class="btn btn-secondary" onclick="closeModal('settings-modal')">Annuler</button>
<button class="btn btn-primary" onclick="saveSettings()">Enregistrer</button>
</div>
</div>
</div>
<script src="/modules/todo/assets/todo.js"></script>
<?php require __DIR__ . '/footer.php'; ?>