const $=s=>document.querySelector(s),KEY="weddash1"; const TCATS=["Planning","Venue","Attire","Food & drink","Photo & video","Flowers & decor","Music","Stationery","Travel","Legal","Other"]; const CATS=["Venue","Catering","Attire","Photography","Flowers","Music","Decor","Stationery","Transport","Other"]; const VT=["Venue","Photographer","Caterer","Florist","Music","Officiant","Cake","Hair & makeup","Planner","Other"]; const COL=["#46725A","#D9959B","#B08A3E","#7E9F85","#C46F5E","#8FB5A0","#E3C28A","#9A7C8C","#6E8F7A","#BFA79A"]; const IC={dash:"M3 11l9-8 9 8M5 10v10h14V10",tasks:"M4 12l5 5L20 6",budget:"M3 7h18v12H3zM3 7l3-3h12v3M16 13h3",guests:"M9 11a3 3 0 100-6 3 3 0 000 6zM3 20c0-3 3-5 6-5s6 2 6 5M17 6a3 3 0 010 5M21 20c0-2-1-4-3-4.5",vendors:"M4 9l1-5h14l1 5M4 9v11h16V9M9 20v-6h6v6",time:"M12 21a9 9 0 100-18 9 9 0 000 18zM12 7v5l3 2",notes:"M6 3h9l4 4v14H6zM9 12h7M9 16h7",set:"M4 7h16M4 12h16M4 17h16",edit:"M4 20h4L19 9l-4-4L4 16zM13 7l4 4",del:"M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13",plus:"M12 5v14M5 12h14"}; const ic=(n,s=20)=>``; const NAV=[["dash","Dashboard"],["tasks","Checklist"],["budget","Budget"],["guests","Guests"],["vendors","Vendors"],["time","Timeline"],["notes","Notes"],["set","Settings"]]; const STARTER=[["Set your budget","Planning","High"],["Draft the guest list","Planning","High"],["Book the venue","Venue","High"],["Hire a photographer","Photo & video","High"],["Book the caterer","Food & drink","Medium"],["Choose the wedding party","Planning","Medium"],["Shop for the dress or suit","Attire","Medium"],["Send save-the-dates","Stationery","Medium"],["Book music or a DJ","Music","Medium"],["Order flowers and decor","Flowers & decor","Low"],["Send invitations","Stationery","High"],["Apply for the marriage licence","Legal","High"]]; const FLD={ tasks:[{k:"t",l:"Task",req:1},{k:"cat",l:"Category",o:TCATS},{k:"due",l:"Due date",t:"date"},{k:"pri",l:"Priority",o:["High","Medium","Low"]}], exp:[{k:"n",l:"Item",req:1},{k:"cat",l:"Category",o:CATS},{k:"e",l:"Estimated cost",t:"number"},{k:"a",l:"Actual cost",t:"number"},{k:"paid",l:"Status",o:["Unpaid","Paid"]}], guests:[{k:"n",l:"Name",req:1},{k:"contact",l:"Phone or email"},{k:"rsvp",l:"RSVP",o:["Pending","Attending","Declined"]},{k:"meal",l:"Meal choice",o:["No preference","Meat","Fish","Vegetarian","Vegan","Kids"]},{k:"plus",l:"Plus-ones",t:"number"},{k:"table",l:"Table"},{k:"note",l:"Notes"}], vendors:[{k:"n",l:"Business name",req:1},{k:"type",l:"Type",o:VT},{k:"contact",l:"Phone or email"},{k:"price",l:"Quote",t:"number"},{k:"st",l:"Status",o:["Researching","Contacted","Booked","Paid in full"]}], events:[{k:"t",l:"Event",req:1},{k:"date",l:"Date",t:"date",req:1},{k:"time",l:"Time",t:"time"},{k:"loc",l:"Location"},{k:"note",l:"Notes"}], set:[{k:"a",l:"Partner one"},{k:"b",l:"Partner two"},{k:"date",l:"Wedding date",t:"date"},{k:"venue",l:"Venue"},{k:"cur",l:"Currency symbol"},{k:"budget",l:"Total budget",t:"number"}]}; const DEF={tasks:{cat:"Planning",pri:"Medium"},exp:{cat:"Venue",paid:"Unpaid"},guests:{rsvp:"Pending",meal:"No preference"},vendors:{type:"Venue",st:"Researching"},events:{},set:{}}; const TTL={tasks:"task",exp:"expense",guests:"guest",vendors:"vendor",events:"event"}; const uid=()=>Math.random().toString(36).slice(2,9); const def=()=>({a:"",b:"",date:"",venue:"",cur:"$",budget:0,tasks:STARTER.map(r=>({id:uid(),t:r[0],cat:r[1],pri:r[2],due:"",done:0})),exp:[],guests:[],vendors:[],events:[],notes:""}); function load(){try{const r=localStorage.getItem(KEY);if(r)return Object.assign(def(),JSON.parse(r))}catch(e){}return def()} function save(){try{localStorage.setItem(KEY,JSON.stringify(S))}catch(e){}} let S=load(),view="dash",flt={},cb=null; const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); const sum=(a,k)=>a.reduce((s,x)=>s+(+x[k]||0),0); const money=n=>S.cur+(+n||0).toLocaleString(undefined,{maximumFractionDigits:0}); const dt=d=>d?new Date(d+"T12:00").toLocaleDateString(undefined,{month:"short",day:"numeric",year:"numeric"}):"\u2014"; const t12=t=>{if(!t)return"";const h=+t.slice(0,2);return (h%12||12)+":"+t.slice(3,5)+(h<12?" am":" pm")}; const today=()=>new Date().toISOString().slice(0,10); const party=g=>1+(+g.plus||0); const names=()=>[S.a,S.b].filter(Boolean).join(" & ")||"Your wedding"; const evs=(a,b)=>(a.date+(a.time||""))<(b.date+(b.time||""))?-1:1; const bar=p=>`
`; const stat=(l,v,s,p)=>`
${l}${v}${p!=null?bar(p):""}${s}
`; const head=(t,s,a)=>`

${t}

${s||""}

${a||""}
`; const addBtn=(c,l)=>``; const acts=(c,id)=>``; const tbl=(cols,rows,empty)=>rows.length?`
${cols.map(c=>``).join("")}${rows.map(r=>`${r.map((x,i)=>``).join("")}`).join("")}
${c}
${x}
`:`
${empty}
`; const cdh=()=>{if(!S.date)return`

Add your wedding date to start the countdown.

`;const ms=new Date(S.date+"T12:00")-Date.now();if(ms<0)return`

Congratulations on your wedding!

`;const d=Math.floor(ms/864e5),h=Math.floor(ms%864e5/36e5),m=Math.floor(ms%36e5/6e4);return[[d,"days"],[h,"hours"],[m,"mins"]].map(x=>`
${x[0]}${x[1]}
`).join("")}; const SPRIG=``; function donut(){const g={};S.exp.forEach(x=>{g[x.cat]=(g[x.cat]||0)+(+x.a||+x.e||0)});const k=Object.keys(g),T=k.reduce((s,c)=>s+g[c],0);if(!T)return`
Add expenses to see your spending split.
`;let off=25;const seg=k.map((c,i)=>{const p=g[c]/T*100,r=``;off-=p;return r}).join("");return`
${seg}
`} const V={}; V.dash=()=>{ const td=S.tasks.filter(x=>x.done).length,tt=S.tasks.length,sp=sum(S.exp,"a"),att=S.guests.filter(g=>g.rsvp==="Attending").reduce((s,g)=>s+party(g),0),bk=S.vendors.filter(v=>/Booked|Paid/.test(v.st)).length; const up=S.tasks.filter(x=>!x.done).sort((a,b)=>(a.due||"9")<(b.due||"9")?-1:1).slice(0,5),ev=S.events.filter(e=>e.date>=today()).sort(evs).slice(0,4); return`
${SPRIG}

${S.date?dt(S.date):"Welcome"}${S.venue?" \u00b7 "+esc(S.venue):""}

${esc(names())}

${cdh()}
${stat("Checklist",`${td}/${tt}`,"tasks done",tt?td/tt*100:0)}${stat("Budget",money(sp),S.budget?`of ${money(S.budget)}`:"spent so far",S.budget?sp/S.budget*100:null)}${stat("Guests",att,`attending \u00b7 ${S.guests.length} invited`)}${stat("Vendors",`${bk}/${S.vendors.length}`,"booked")}

Upcoming tasks

${up.length?up.map(x=>`
${esc(x.t)}
${x.due?"Due "+dt(x.due):x.cat}
`).join(""):`
All caught up.
`}

Budget by category

${donut()}

Coming up

${ev.length?ev.map(e=>`
${esc(e.t)}
${dt(e.date)} ${t12(e.time)}
`).join(""):`
No upcoming events. Add some on the Timeline.
`}

Quick add

${addBtn("tasks","Task")}${addBtn("guests","Guest")}${addBtn("exp","Expense")}${addBtn("vendors","Vendor")}${addBtn("events","Event")}
`}; const tHtml=()=>{const q=(flt.tq||"").toLowerCase(),c=flt.tc||"";const r=S.tasks.filter(x=>(!q||x.t.toLowerCase().includes(q))&&(!c||x.cat===c));return tbl(["","Task","Category","Due","Priority",""],r.map(x=>[``,`${esc(x.t)}`,esc(x.cat),dt(x.due),`${x.pri}`,acts("tasks",x.id)]),"No tasks match. Add one to get going.")}; V.tasks=()=>{const td=S.tasks.filter(x=>x.done).length,tt=S.tasks.length;return head("Checklist",`${td} of ${tt} tasks done`,addBtn("tasks","Add task"))+`
${bar(tt?td/tt*100:0)}
${tHtml()}
`}; V.budget=()=>{const e=sum(S.exp,"e"),a=sum(S.exp,"a"),left=S.budget-a;return head("Budget",`${S.exp.filter(x=>x.paid==="Paid").length} of ${S.exp.length} items paid`,addBtn("exp","Add expense"))+`
${stat("Total budget",money(S.budget),"set in details")}${stat("Estimated",money(e),"planned costs")}${stat("Spent",money(a),"actual costs",S.budget?a/S.budget*100:null)}${stat(left<0?"Over budget":"Remaining",money(Math.abs(left)),S.budget?"of your budget":"set a budget first")}

Spending split

${donut()}
${tbl(["Item","Category","Estimated","Actual","Status",""],S.exp.map(x=>[esc(x.n),esc(x.cat),money(x.e),money(x.a),``,acts("exp",x.id)]),"No expenses yet. Add your first one.")}
`}; const gHtml=()=>{const q=(flt.gq||"").toLowerCase(),r=flt.gr||"";const l=S.guests.filter(g=>(!q||g.n.toLowerCase().includes(q))&&(!r||g.rsvp===r));return tbl(["Name","Contact","RSVP","Meal","Party","Table",""],l.map(g=>[`${esc(g.n)}${g.note?`
${esc(g.note)}`:""}`,esc(g.contact)||"\u2014",``,esc(g.meal),party(g),esc(g.table)||"\u2014",acts("guests",g.id)]),"No guests match. Add your first guest.")}; V.guests=()=>{const c=r=>S.guests.filter(g=>g.rsvp===r).reduce((s,g)=>s+party(g),0);return head("Guest list",`${S.guests.length} invitations`,addBtn("guests","Add guest"))+`
${stat("Invited",S.guests.reduce((s,g)=>s+party(g),0),"people in total")}${stat("Attending",c("Attending"),"confirmed")}${stat("Pending",c("Pending"),"awaiting reply")}${stat("Declined",c("Declined"),"can\u2019t make it")}
${gHtml()}
`}; V.vendors=()=>head("Vendors",`${S.vendors.length} on your list`,addBtn("vendors","Add vendor"))+(S.vendors.length?`
${S.vendors.map(v=>`
${esc(v.type)}${acts("vendors",v.id)}

${esc(v.n)}

${esc(v.contact)||"No contact yet"}

${v.price?money(v.price):"No quote"}
`).join("")}
`:`
No vendors yet. Add the first one you are considering.
`); V.time=()=>head("Timeline","Milestones and the schedule for the big day",addBtn("events","Add event"))+`
${S.events.length?S.events.slice().sort(evs).map(e=>{const d=new Date(e.date+"T12:00");return`
${d.getDate()}${d.toLocaleDateString(undefined,{month:"short"})}

${esc(e.t)}

${[t12(e.time),esc(e.loc)].filter(Boolean).join(" \u00b7 ")||"\u00a0"}

${e.note?`

${esc(e.note)}

`:""}
${acts("events",e.id)}
`}).join(""):`
Your timeline is empty. Add the first event.
`}
`; V.notes=()=>head("Notes","Ideas, inspiration and reminders. Saved as you type.")+`
`; V.set=()=>head("Settings","Details, backups and data")+`

Wedding details

${esc(names())}${S.date?" \u00b7 "+dt(S.date):""}

Backup and restore

Your data is stored in this browser only. Download a backup file, or copy it as text, to keep it safe.

Data

`; const side=()=>`
Wedding Planner${esc(names())}
`; function render(){$("#wpside").innerHTML=side();$("#wpmain").innerHTML=V[view]()} function form(c,id){const it=c==="set"?S:id?S[c].find(x=>x.id===id):Object.assign({},DEF[c]);$("#wpmodal").innerHTML=`

${c==="set"?"Wedding details":(id?"Edit ":"Add ")+TTL[c]}

${FLD[c].map(f=>``).join("")}
`;const i=$("#wpmodal input,#modal select");if(i)i.focus()} function ask(m,fn){cb=fn;$("#wpmodal").innerHTML=`

${m}

`} function close(){$("#wpmodal").innerHTML="";cb=null} function sample(){const d=n=>new Date(Date.now()+n*864e5).toISOString().slice(0,10),o=def();Object.assign(o,{a:"Sophie",b:"Daniel",date:d(150),venue:"Willow Garden Estate",cur:S.cur,budget:20000, exp:[["Venue","Venue",5000,5000,"Paid"],["Catering","Catering",4500,0,"Unpaid"],["Photographer","Photography",2200,500,"Unpaid"],["Florist","Flowers",1200,0,"Unpaid"],["Wedding dress","Attire",1800,1650,"Paid"]].map(r=>({id:uid(),n:r[0],cat:r[1],e:r[2],a:r[3],paid:r[4]})), guests:[["Olivia Bennett","olivia@email.com","Attending","Meat",1,"1"],["Noah Carter","555-0142","Attending","Vegetarian",0,"1"],["Mia Foster","","Pending","No preference",1,""],["Liam Brooks","","Declined","No preference",0,""]].map(r=>({id:uid(),n:r[0],contact:r[1],rsvp:r[2],meal:r[3],plus:r[4],table:r[5],note:""})), vendors:[["Willow Garden Estate","Venue","hello@willow.example",5000,"Paid in full"],["Bright Lens Studio","Photographer","555-0188",2200,"Booked"],["Petal & Stem","Florist","",1200,"Contacted"]].map(r=>({id:uid(),n:r[0],type:r[1],contact:r[2],price:r[3],st:r[4]})), events:[["Rehearsal dinner",149,"18:00","The Garden Bistro"],["Hair and makeup",150,"09:00","Bridal suite"],["Ceremony",150,"15:00","Willow Garden"],["Reception",150,"17:30","Grand Ballroom"]].map(r=>({id:uid(),t:r[0],date:d(r[1]),time:r[2],loc:r[3],note:""}))}); o.tasks.forEach((t,i)=>{t.due=d(10+i*10);t.done=i<3?1:0});return o} function imp(txt){try{S=Object.assign(def(),JSON.parse(txt));save();render();$("#msg").textContent="Backup restored."}catch(e){const m=$("#msg");if(m)m.textContent="That backup could not be read. Check you used the full text or file."}} document.addEventListener("click",e=>{const b=e.target.closest("[data-a]");if(!b)return;const a=b.dataset.a.split(":"),k=a[0]; if(k==="nav"){view=a[1];render();scrollTo(0,0);return} if(k==="add"){form(a[1]);return} if(k==="edit"){form(a[1],a[2]);return} if(k==="close"){close();return} if(k==="del"){ask("Delete this item?",()=>{S[a[1]]=S[a[1]].filter(x=>x.id!==a[2]);save();render()});return} if(k==="yes"){const f=cb;close();if(f)f();return} if(k==="tog"){const t=S.tasks.find(x=>x.id===a[1]);t.done=t.done?0:1} else if(k==="tgp"){const x=S.exp.find(y=>y.id===a[1]);x.paid=x.paid==="Paid"?"Unpaid":"Paid"} else if(k==="rsvp"){const g=S.guests.find(y=>y.id===a[1]);g.rsvp={Pending:"Attending",Attending:"Declined",Declined:"Pending"}[g.rsvp]} else if(k==="vst"){const v=S.vendors.find(y=>y.id===a[1]),o=["Researching","Contacted","Booked","Paid in full"];v.st=o[(o.indexOf(v.st)+1)%4]} else if(k==="dl"){const u=URL.createObjectURL(new Blob([JSON.stringify(S,null,1)],{type:"application/json"})),x=document.createElement("a");x.href=u;x.download="wedding-planner-backup.json";document.body.appendChild(x);x.click();x.remove();setTimeout(()=>URL.revokeObjectURL(u),500);$("#msg").textContent="If nothing downloaded, use Show backup text and copy it instead.";return} else if(k==="show"){$("#bk").value=JSON.stringify(S);$("#msg").textContent="Copy all the text above and keep it somewhere safe.";return} else if(k==="rest"){imp($("#bk").value);return} else if(k==="sample"){ask("Replace your current data with sample data?",()=>{S=sample();save();render()});return} else if(k==="reset"){ask("Erase everything and start fresh?",()=>{S=def();save();render()});return} else return; save();render()}); document.addEventListener("submit",e=>{e.preventDefault();const f=e.target;if(!f.dataset.f)return;const p=f.dataset.f.split(":"),c=p[0],id=p[1],d=Object.fromEntries(new FormData(f));FLD[c].forEach(x=>{if(x.t==="number")d[x.k]=+d[x.k]||0;else if(typeof d[x.k]==="string")d[x.k]=d[x.k].trim()}); if(c==="set"){if(!d.cur)d.cur="$";Object.assign(S,d)}else if(id)Object.assign(S[c].find(x=>x.id===id),d);else S[c].push(Object.assign({id:uid()},d)); save();close();render()}); document.addEventListener("input",e=>{const t=e.target;if(t.id==="nt"){S.notes=t.value;save()}else if(t.id==="tq"||t.id==="gq"){flt[t.id]=t.value;$("#tb").innerHTML=(t.id==="tq"?tHtml:gHtml)()}}); document.addEventListener("change",e=>{const t=e.target;if(t.id==="tc"||t.id==="gr"){flt[t.id]=t.value;$("#tb").innerHTML=(t.id==="tc"?tHtml:gHtml)()}else if(t.id==="imp"&&t.files[0]){const r=new FileReader();r.onload=()=>imp(r.result);r.readAsText(t.files[0])}}); document.addEventListener("keydown",e=>{if(e.key==="Escape")close()}); setInterval(()=>{const e=$("#cd");if(e)e.innerHTML=cdh()},60000); render();