Task list with optional assignee; fix spending/task form submit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6866316925
commit
e013963bf7
2 files changed
+83
-4
No files matched your search
+51
-3
@@ -260,7 +260,7 @@ function bestName(ref){
|
||||
}
|
||||
|
||||
// ---------- state ----------
|
||||
const EMPTY=()=>({parts:{}, links:{}, comments:{}, drawings:{}, photos:{}, users:[], me:"", spending:[], activity:[]});
|
||||
const EMPTY=()=>({parts:{}, links:{}, comments:{}, drawings:{}, photos:{}, users:[], me:"", spending:[], activity:[], tasks:[]});
|
||||
let state = EMPTY();
|
||||
let current=GROUPS[0].key, selRef=null, filter="all", query="", zoom=1, editing=false, confirmBulk=false, online=false;
|
||||
const pid=(g,r)=>g+"__"+r;
|
||||
@@ -366,6 +366,7 @@ function renderTabs(){
|
||||
<span class="bar">${["have","skip","ord","miss"].map(s=>`<i style="width:${c[s]/c.total*100}%;background:var(--${s})"></i>`).join("")}</span>
|
||||
<span class="m"><span>${done}/${c.total} done</span><span>${c.miss?`<span style="color:var(--miss)">${c.miss} missing</span>`:(imageFor(G.key)?"drawing":"no drawing yet")}</span></span></button>`;
|
||||
}).join("") + `<button class="tab photos" role="tab" aria-selected="${current==="gallery"}" data-g="gallery"><span class="t">Photos</span><span class="m"><span>${Object.values(state.photos||{}).reduce((n,l)=>n+l.length,0)} photos</span><span></span></span></button>`
|
||||
+ `<button class="tab" role="tab" aria-selected="${current==="tasks"}" data-g="tasks"><span class="t">Tasks</span><span class="m"><span>${(state.tasks||[]).filter(t=>!t.done).length} open</span><span>${(state.tasks||[]).filter(t=>!t.done&&t.to===state.me).length?`<span style="color:var(--accent)">${(state.tasks||[]).filter(t=>!t.done&&t.to===state.me).length} yours</span>`:""}</span></span></button>`
|
||||
+ `<button class="tab" role="tab" aria-selected="${current==="spending"}" data-g="spending"><span class="t">Spending</span><span class="m"><span>${eur((state.spending||[]).reduce((n,x)=>n+x.cents,0))}</span><span></span></span></button>`
|
||||
+ `<button class="tab" role="tab" aria-selected="${current==="activity"}" data-g="activity"><span class="t">Activity</span><span class="m"><span>who did what</span><span></span></span></button>`
|
||||
+ `<button class="tab shop" role="tab" aria-selected="${current==="shop"}" data-g="shop"><span class="t">Shopping list</span><span class="m"><span>${shopN} parts to get</span><span></span></span></button>`;
|
||||
@@ -381,6 +382,7 @@ function visibleRefs(G){
|
||||
function renderMain(){
|
||||
if(current==="shop") return renderShop();
|
||||
if(current==="gallery") return renderGallery();
|
||||
if(current==="tasks") return renderTasks();
|
||||
if(current==="spending") return renderSpending();
|
||||
if(current==="activity") return renderActivity();
|
||||
const G=GROUPS.find(x=>x.key===current);
|
||||
@@ -482,6 +484,9 @@ document.addEventListener("click",async e=>{
|
||||
if(e.target.closest("#stage") && !editing && !e.target.closest(".pin")){ openDrawing(current); return; }
|
||||
const t=e.target.closest("button,[data-g]"); if(!t) return;
|
||||
if(t.dataset.go){ const [g,r]=t.dataset.go.split("|"); current=g; selRef=r; filter="all"; render(); try{localStorage.setItem(LS+"-tab",current)}catch(_){} return; }
|
||||
if(t.dataset.tf){ taskFilter=t.dataset.tf; render(); return; }
|
||||
if(t.dataset.deltask){ const id=+t.dataset.deltask; if(!confirm("Remove this task?")) return;
|
||||
try{ const r=await send("DELETE","api/tasks/"+id,undefined,()=>{ state.tasks=state.tasks.filter(x=>x.id!==id); }); if(!r.queued) await reload(); }catch(err){ toast("Couldn’t remove: "+err.message); } return; }
|
||||
if(t.dataset.delspend){ const id=+t.dataset.delspend; if(!confirm("Remove this spending entry?")) return;
|
||||
try{ const r=await send("DELETE","api/spending/"+id,undefined,()=>{ state.spending=state.spending.filter(x=>x.id!==id); }); if(!r.queued) await reload(); }catch(err){ toast("Couldn’t remove: "+err.message); } return; }
|
||||
if(t.dataset.ph){ const [sc,i]=t.dataset.ph.split("|"); openPhotos(photoItems(sc),+i); return; }
|
||||
@@ -506,7 +511,13 @@ document.addEventListener("click",async e=>{
|
||||
document.addEventListener("input",e=>{
|
||||
if(e.target.id==="q"){ query=e.target.value.trim(); const pos=e.target.selectionStart; renderMain(); const q=$("#q"); q.focus(); q.setSelectionRange(pos,pos); }
|
||||
});
|
||||
async function taskUpdate(id,patch){
|
||||
try{ const r=await send("PUT","api/tasks/"+id,patch,()=>{ const t=state.tasks.find(x=>x.id===id); if(t){ if(patch.done!==undefined){ t.done=patch.done; t.doneBy=patch.done?state.me:""; t.doneAt=patch.done?Date.now():0; } if(patch.to!==undefined) t.to=patch.to; } });
|
||||
if(!r.queued) await reload(); }catch(err){ toast("Couldn’t save: "+err.message); await reload(); }
|
||||
}
|
||||
document.addEventListener("change",async e=>{
|
||||
if(e.target.dataset.tdone){ taskUpdate(+e.target.dataset.tdone,{done:e.target.checked}); return; }
|
||||
if(e.target.dataset.tto){ taskUpdate(+e.target.dataset.tto,{to:e.target.value}); return; }
|
||||
if(e.target.id==="note"){ savePart(current,selRef,{note:e.target.value}); }
|
||||
if((e.target.id==="pupl"||e.target.id==="gupl") && e.target.files.length){
|
||||
const files=[...e.target.files], scope=e.target.id==="gupl"?"gallery":pid(current,selRef), cap=e.target.id==="gupl"?($("#gcap")||{}).value:"";
|
||||
@@ -519,7 +530,7 @@ document.addEventListener("change",async e=>{
|
||||
}
|
||||
});
|
||||
document.addEventListener("submit",async e=>{
|
||||
const f=e.target.closest("form[data-form]"); if(!f) return; e.preventDefault(); if(f.dataset.form==="spend") return;
|
||||
const f=e.target.closest("form[data-form]"); if(!f) return; e.preventDefault(); if(f.dataset.form==="spend"||f.dataset.form==="task") return;
|
||||
const id=pid(current,selRef);
|
||||
try{
|
||||
let r;
|
||||
@@ -530,6 +541,22 @@ document.addEventListener("submit",async e=>{
|
||||
if(!r.queued) await reload();
|
||||
}catch(err){ toast(err.message); }
|
||||
});
|
||||
// spending + task forms
|
||||
document.addEventListener("submit",async e=>{
|
||||
const f=e.target.closest('form[data-form="spend"],form[data-form="task"]'); if(!f) return; e.preventDefault();
|
||||
try{
|
||||
let r;
|
||||
if(f.dataset.form==="spend"){
|
||||
const b={amount:$("#samt").value,what:$("#swhat").value}; const cents=Math.round(Number(b.amount.replace(",","."))*100);
|
||||
if(!(cents>0)){ toast("Enter an amount greater than 0"); return; }
|
||||
r=await send("POST","api/spending",b,()=>{ state.spending.unshift({id:tmpId--,by:state.me,cents,what:b.what,at:Date.now()}); }); toast(r.queued?"Saved offline, will upload later":"Added");
|
||||
} else {
|
||||
const b={title:$("#ttitle").value,to:$("#tto").value};
|
||||
r=await send("POST","api/tasks",b,()=>{ state.tasks.unshift({id:tmpId--,title:b.title,to:b.to,done:false,by:state.me,at:Date.now(),doneBy:"",doneAt:0}); }); toast(r.queued?"Saved offline, will upload later":"Task added");
|
||||
}
|
||||
if(!r.queued) await reload();
|
||||
}catch(err){ toast(err.message); }
|
||||
});
|
||||
// marker drag + place
|
||||
let drag=null, dragMoved=false;
|
||||
document.addEventListener("pointerdown",e=>{
|
||||
@@ -564,6 +591,11 @@ function actText(a){
|
||||
case "comment_del": return "deleted a comment";
|
||||
case "photo_add": return "added a photo"+(d?` “${d}”`:"");
|
||||
case "photo_del": return "deleted a photo";
|
||||
case "task_add": return `added the task “${d}”`;
|
||||
case "task_done": return `finished “${d}”`;
|
||||
case "task_open": return `reopened “${d}”`;
|
||||
case "task_assign": return `assigned “${d}”`;
|
||||
case "task_del": return `removed the task “${d}”`;
|
||||
case "markers": return "moved markers on "+groupTitle(d);
|
||||
case "spend_add": { const [m,...w]=d.split(" · "); return `spent €${m} on ${w.join(" · ")}`; }
|
||||
case "spend_del": { const [m,...w]=d.split(" · "); return `removed the spending €${m} ${w.join(" · ")}`; }
|
||||
@@ -575,6 +607,22 @@ function settlements(bal){
|
||||
let i=0,j=0; while(i<give.length&&j<get.length){ const m=Math.min(give[i].v,get[j].v); out.push({from:give[i].n,to:get[j].n,c:Math.round(m)}); give[i].v-=m; get[j].v-=m; if(give[i].v<0.5) i++; if(get[j].v<0.5) j++; }
|
||||
return out;
|
||||
}
|
||||
let taskFilter="open";
|
||||
function renderTasks(){
|
||||
const all=state.tasks||[], me=state.me, names=(state.users||[]).map(u=>u.name);
|
||||
const list=all.filter(t=>taskFilter==="all"||(taskFilter==="open"&&!t.done)||(taskFilter==="mine"&&!t.done&&t.to===me)||(taskFilter==="done"&&t.done));
|
||||
const sel=(v,id,extra="")=>`<select ${id?`id="${id}"`:""} ${extra}><option value="">Anyone</option>${names.map(n=>`<option value="${esc(n)}"${n===v?" selected":""}>${esc(n)}</option>`).join("")}</select>`;
|
||||
$("#main").innerHTML=`<section class="card shop">
|
||||
<div><h2 style="font-size:20px">Tasks</h2><p class="rsub" style="margin:4px 0 0">A relaxed to-do list for the build. Assign something to someone, or leave it open for anyone.</p></div>
|
||||
<form class="addrow" data-form="task"><input type="text" id="ttitle" placeholder="e.g. Strip the engine, order gaskets…" required maxlength="200" autocomplete="off">${sel("","tto")}<button class="btn primary" type="submit">Add</button></form>
|
||||
<div class="filters">${[["open","Open"],["mine","Mine"],["done","Done"],["all","All"]].map(([k,l])=>`<button class="btn${taskFilter===k?" on":""}" data-tf="${k}">${l}</button>`).join("")}</div>
|
||||
<div class="sub">${list.map(t=>`<div class="item" style="grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center">
|
||||
<input type="checkbox" style="width:22px;height:22px" data-tdone="${t.id}" ${t.done?"checked":""} ${t.id<0?"disabled":""} aria-label="Done">
|
||||
<span style="${t.done?"text-decoration:line-through;color:var(--muted)":""}"><b>${esc(t.title)}</b><br><span class="when">${t.done?`done by ${esc(t.doneBy)} · ${when(t.doneAt)}`:`added by ${esc(t.by)} · ${when(t.at)}`}${t.id<0?" · waiting to upload":""}</span></span>
|
||||
${t.id>0&&!t.done?sel(t.to,"",`data-tto="${t.id}" aria-label="Assigned to"`):`<span class="chip">${esc(t.to||"anyone")}</span>`}
|
||||
${t.id>0?`<button class="x" data-deltask="${t.id}" aria-label="Remove task">×</button>`:"<span></span>"}</div>`).join("")||`<div class="empty">${taskFilter==="mine"?"Nothing assigned to you.":"No tasks here."}</div>`}</div>
|
||||
</section>`;
|
||||
}
|
||||
function renderSpending(){
|
||||
const sp=state.spending||[]; const names=(state.users||[]).map(u=>u.name); const n=Math.max(1,names.length);
|
||||
const total=sp.reduce((a,x)=>a+x.cents,0), share=total/n;
|
||||
@@ -757,7 +805,7 @@ document.addEventListener("click",async e=>{
|
||||
})();
|
||||
|
||||
// ---------- boot ----------
|
||||
try{ const t=localStorage.getItem(LS+"-tab"); if(t && (t==="shop"||t==="gallery"||t==="spending"||t==="activity"||GROUPS.some(g=>g.key===t))) current=t; }catch(_){}
|
||||
try{ const t=localStorage.getItem(LS+"-tab"); if(t && (t==="shop"||t==="gallery"||t==="tasks"||t==="spending"||t==="activity"||GROUPS.some(g=>g.key===t))) current=t; }catch(_){}
|
||||
if("serviceWorker" in navigator) navigator.serviceWorker.register("sw.js").catch(()=>{});
|
||||
window.addEventListener("online",()=>reload());
|
||||
window.addEventListener("offline",()=>{ online=false; badgeNow(); });
|
||||
|
||||
Reference in new issue
Block a user