diff --git a/modules/todo/api.php b/modules/todo/api.php index 86c5e7f..9bfd1d7 100644 --- a/modules/todo/api.php +++ b/modules/todo/api.php @@ -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); diff --git a/modules/todo/assets/todo.js b/modules/todo/assets/todo.js index 9a652ff..a8a1b6f 100644 --- a/modules/todo/assets/todo.js +++ b/modules/todo/assets/todo.js @@ -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=` -
Vue
+ let html=` +
Vues
- 📋 Toutes ${stats.pending}
+ 📋 Toutes + ${stats.pending||0} +
- 📅 Aujourd'hui ${stats.today}
+ ☀️ Aujourd'hui + ${stats.today||0} +
- 🗓️ À venir
`; + 📅 À venir + `; if(parseInt(stats.overdue)>0){ - smartHtml+=`
- 🔴 En retard ${stats.overdue}
`; + html+=`
+ ⚠️ En retard + ${stats.overdue} +
`; } - smartHtml+=`
- ✅ Terminées ${stats.done}
`; + html+=`
+ Terminées + ${stats.done||0} +
`; - // Lists - let listsHtml='
Listes
'; + html+=`
+ Listes +
`; lists.forEach(l=>{ const act=currentFilter==='list_'+l.id?' active':''; - listsHtml+=`
+ html+=`
${escHtml(l.icon)} ${escHtml(l.name)} ${l.pending||0} +
`; }); - 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&¤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');} } @@ -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=''+ lists.map(l=>``).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=`
${currentFilter==='done'?'✅':'📋'}
-

${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche. Ajoutez-en une !'}

`; +

${currentFilter==='done'?'Aucune tâche terminée.':'Aucune tâche pour le moment.'}

`; 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+=`
- Terminées (${done.length})
`; - done.forEach(t=>{ html+=todoItemHtml(t); }); - } else if(done.length&&!showDone){ + html+=`
Terminées (${done.length})
`; + done.forEach(t=>{html+=todoItemHtml(t);}); + }else if(done.length&&!showDone&¤tFilter!=='done'){ html+=`
`; } @@ -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'? `${t.priority==='high'?'Urgent':t.priority==='medium'?'Normal':'Bas'}`:''; const listBadge=t.list_name? `${escHtml(t.list_icon||'')} ${escHtml(t.list_name)}`:''; - return `
-
+ return `
+
${escHtml(t.title)}
${t.notes?`
${escHtml(t.notes)}
`:''} @@ -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=''+lists.map(l=>``).join(''); - if(currentFilter.startsWith('list_')) sel.value=currentFilter.slice(5); + sel.innerHTML=''+lists.map(l=>``).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=''+lists.map(l=>``).join(''); + sel.innerHTML=''+lists.map(l=>``).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=> `
`).join(''); - // Render icons + document.getElementById('list-icon-opts').innerHTML=ICONS.map(i=> - ``).join(''); + ``).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');})); }); diff --git a/todo.php b/todo.php index df2178c..3a9e1c3 100644 --- a/todo.php +++ b/todo.php @@ -14,40 +14,12 @@ require __DIR__ . '/header.php';
-