๐Ÿ“
Loading...
๐Ÿ“

Pickleball Squad Scheduler

Mon
Tue
Wed
Thu
Fri
Sat
Sun

๐Ÿ‘† Tap a date to set available hours

const firebaseConfig={apiKey:"AIzaSyC06tFY1YDqbdm5LP3khJE99aNB6A4bJPs",authDomain:"pickleball-squad.firebaseapp.com",databaseURL:"https://pickleball-squad-default-rtdb.asia-southeast1.firebasedatabase.app",projectId:"pickleball-squad",storageBucket:"pickleball-squad.firebasestorage.app",messagingSenderId:"1005560481377",appId:"1:1005560481377:web:dfdc0ef9352b8c21710c20"}; const PLAYERS=["GJ","Faye","Roxanne","Chelsea","Mye","Luis"]; const PLAYER_COLORS=["#10b981","#f59e0b","#ef4444","#3b82f6","#8b5cf6","#ec4899"]; const MONTH_NAMES=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const FULL_MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"]; const DAY_NAMES=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]; const HOURS=[ {key:"7-8AM",label:"7โ€“8 AM",period:"morning"},{key:"8-9AM",label:"8โ€“9 AM",period:"morning"}, {key:"9-10AM",label:"9โ€“10 AM",period:"morning"},{key:"10-11AM",label:"10โ€“11 AM",period:"morning"}, {key:"11-12PM",label:"11 AMโ€“12 PM",period:"morning"},{key:"12-1PM",label:"12โ€“1 PM",period:"afternoon"}, {key:"1-2PM",label:"1โ€“2 PM",period:"afternoon"},{key:"2-3PM",label:"2โ€“3 PM",period:"afternoon"}, {key:"3-4PM",label:"3โ€“4 PM",period:"afternoon"},{key:"4-5PM",label:"4โ€“5 PM",period:"afternoon"}, {key:"5-6PM",label:"5โ€“6 PM",period:"afternoon"},{key:"6-7PM",label:"6โ€“7 PM",period:"evening"}, {key:"7-8PM",label:"7โ€“8 PM",period:"evening"},{key:"8-9PM",label:"8โ€“9 PM",period:"evening"}, {key:"9-10PM",label:"9โ€“10 PM",period:"evening"},{key:"10-11PM",label:"10โ€“11 PM",period:"evening"}, {key:"11-12MN",label:"11 PMโ€“12 MN",period:"evening"} ]; const PERIODS=[{key:"morning",label:"๐ŸŒ… Morning",color:"#f59e0b"},{key:"afternoon",label:"โ˜€๏ธ Afternoon",color:"#3b82f6"},{key:"evening",label:"๐ŸŒ™ Evening",color:"#8b5cf6"}]; let availability={},bookings={},openPlays={}; let activePlayer=PLAYERS[0],selectedDateKey=null; let weekOffset=0,summaryWeekOffset=0; // payStatus: {playerName: true=pays, false=no pay} let bookingPayStatus={}; let opSelectedPlayers=new Set(); let db; const TODAY=new Date();TODAY.setHours(0,0,0,0); function dateKey(d){return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;} function parseKey(k){const[y,m,d]=k.split('-').map(Number);const dt=new Date(y,m-1,d);dt.setHours(0,0,0,0);return dt;} // Monday-first window function getWindowStart(o){ const d=new Date(TODAY); const day=d.getDay(); const diff=day===0?-6:1-day; // back to Monday d.setDate(d.getDate()+diff+o*14); return d; } function getWindowDays(o){const s=getWindowStart(o);return Array.from({length:14},(_,i)=>{const d=new Date(s);d.setDate(s.getDate()+i);return d;});} function getWindowLabel(o){const d=getWindowDays(o);const s=d[0],e=d[13];const sm=MONTH_NAMES[s.getMonth()],em=MONTH_NAMES[e.getMonth()];if(sm===em&&s.getFullYear()===e.getFullYear())return`${sm} ${s.getDate()}โ€“${e.getDate()}, ${s.getFullYear()}`;return`${sm} ${s.getDate()} โ€“ ${em} ${e.getDate()}, ${e.getFullYear()}`;} function getWindowSub(o){if(o===0)return"Current 2 weeks";if(o===1)return"Next 2 weeks";if(o===-1)return"Previous 2 weeks";return o>0?`${o*2} weeks ahead`:`${Math.abs(o)*2} weeks ago`;} function getHourColor(key){const h=HOURS.find(h=>h.key===key);if(!h)return"#94a3b8";return PERIODS.find(p=>p.key===h.period)?.color||"#94a3b8";} function setStatus(msg,color){const el=document.getElementById("save-status");el.textContent=msg;el.style.color=color||"#10b981";} function ensureDate(k){if(!availability[k]){availability[k]={};PLAYERS.forEach(p=>availability[k][p]=new Set());}PLAYERS.forEach(p=>{if(!availability[k][p])availability[k][p]=new Set();});} function uid(){return Date.now().toString(36)+Math.random().toString(36).substr(2);} function formatPHP(n){return 'โ‚ฑ'+Number(n).toLocaleString();} // Firebase init firebase.initializeApp(firebaseConfig); db=firebase.database(); const loadingTimeout=setTimeout(()=>{hideLoading();setStatus("โš  Offline","#f59e0b");renderAll();},5000); db.ref("pickleball-v3").on("value",snap=>{ clearTimeout(loadingTimeout); const d=snap.val();availability={}; if(d){Object.keys(d).forEach(k=>{availability[k]={};PLAYERS.forEach(p=>{availability[k][p]=new Set(d[k][p]||[]);});});} hideLoading();renderAll();setStatus("โ— Live","#10b981"); },()=>{clearTimeout(loadingTimeout);hideLoading();setStatus("โš  Offline","#f59e0b");renderAll();}); db.ref("bookings").on("value",snap=>{bookings=snap.val()||{};renderBookings();}); db.ref("openplays").on("value",snap=>{openPlays=snap.val()||{};renderOpenPlays();}); function hideLoading(){document.getElementById("loading-screen").style.display="none";document.getElementById("app-screen").style.display="block";} function saveAvail(){ setStatus("โณ Saving...","#f59e0b"); const data={}; Object.keys(availability).forEach(k=>{PLAYERS.forEach(p=>{const s=availability[k][p];if(s&&s.size>0){if(!data[k])data[k]={};data[k][p]=[...s];}});}); db.ref("pickleball-v3").set(data).then(()=>setStatus("โœ“ Saved "+new Date().toLocaleTimeString(),"#10b981")).catch(()=>setStatus("โœ— Failed","#ef4444")); } function switchView(v){ document.querySelectorAll(".view").forEach(el=>el.classList.remove("active")); document.querySelectorAll(".btab").forEach(el=>el.classList.remove("active")); document.getElementById("view-"+v).classList.add("active"); document.getElementById("btab-"+v).classList.add("active"); if(v==="best")renderSummary(); if(v==="bookings")renderBookings(); if(v==="openplay")renderOpenPlays(); } // โ”€โ”€ AVAILABILITY โ”€โ”€ function shiftWeek(d){weekOffset+=d;selectedDateKey=null;renderWeekNav();renderCalendar();renderTimePicker();} function shiftWeekSummary(d){summaryWeekOffset+=d;renderSummary();} function playerHourCount(p,days){return days.reduce((s,d)=>s+(availability[dateKey(d)]?.[p]?.size||0),0);} function renderWeekNav(){ document.getElementById("week-label").textContent=getWindowLabel(weekOffset); document.getElementById("week-sub").textContent=getWindowSub(weekOffset); } function renderPlayerGrid(){ const grid=document.getElementById("player-grid");grid.innerHTML=""; const days=getWindowDays(weekOffset); PLAYERS.forEach((p,i)=>{ const count=playerHourCount(p,days),isActive=p===activePlayer; const btn=document.createElement("button");btn.className="player-btn"; btn.style.borderColor=isActive?PLAYER_COLORS[i]:"transparent"; btn.style.background=isActive?PLAYER_COLORS[i]+"22":"#1e293b"; btn.style.color=isActive?PLAYER_COLORS[i]:"#64748b"; btn.textContent=p;btn.onclick=()=>{activePlayer=p;selectedDateKey=null;renderPlayerGrid();renderCalendar();renderTimePicker();}; if(count>0){const b=document.createElement("span");b.className="player-badge";b.style.background=PLAYER_COLORS[i];b.textContent=count;btn.appendChild(b);} grid.appendChild(btn); }); } function renderCalendar(){ const grid=document.getElementById("cal-grid");grid.innerHTML=""; const days=getWindowDays(weekOffset),pc=PLAYER_COLORS[PLAYERS.indexOf(activePlayer)]; // Pad for Monday-first (Mon=0 offset) const firstDay=days[0].getDay(); // 0=Sun,1=Mon... const padCount=firstDay===0?6:firstDay-1; for(let i=0;i{ const k=dateKey(d);ensureDate(k); const slots=availability[k][activePlayer],hasAny=slots.size>0; const isSelected=selectedDateKey===k,isToday=d.getTime()===TODAY.getTime(),isWeekend=d.getDay()===0||d.getDay()===6,showMonth=d.getDate()===1||idx===0; const cell=document.createElement("div");cell.className="cal-day"; cell.style.border=isSelected?`2px solid ${pc}`:isToday?`2px solid #10b981`:`1px solid ${hasAny?pc+"55":"#334155"}`; cell.style.background=isSelected?pc+"33":isToday?"#10b98118":hasAny?pc+"11":"#0f172a"; if(showMonth){const mo=document.createElement("span");mo.className="day-month";mo.textContent=MONTH_NAMES[d.getMonth()];cell.appendChild(mo);} const num=document.createElement("span");num.className="day-num";num.style.color=isToday?"#10b981":isWeekend?"#f59e0b":"#94a3b8";num.textContent=d.getDate();cell.appendChild(num); const cnt=document.createElement("span");cnt.className="slot-count";cnt.style.color=hasAny?pc:"#334155";cnt.textContent=hasAny?slots.size+"h":"ยท";cell.appendChild(cnt); if(hasAny){const dot=document.createElement("div");dot.className="dot";dot.style.background=pc;cell.appendChild(dot);} cell.onclick=()=>{selectedDateKey=selectedDateKey===k?null:k;renderCalendar();renderTimePicker();}; grid.appendChild(cell); }); } function renderTimePicker(){ const wrap=document.getElementById("timepicker-wrap"),hint=document.getElementById("tap-hint"); if(!selectedDateKey){wrap.innerHTML="";hint.style.display="block";hint.textContent="๐Ÿ‘† Tap a date to set hours for "+activePlayer;return;} hint.style.display="none";ensureDate(selectedDateKey); const d=parseKey(selectedDateKey),label=`${DAY_NAMES[d.getDay()]}, ${FULL_MONTHS[d.getMonth()]} ${d.getDate()}`; let html=`

${activePlayer} โ€” ${label}

Tap hour blocks you're free

`; PERIODS.forEach(period=>{html+=`
${period.label}
`;}); html+=`
`;wrap.innerHTML=html; PERIODS.forEach(period=>{ const container=document.getElementById(`hg-${period.key}`); HOURS.filter(h=>h.period===period.key).forEach(h=>{ const active=availability[selectedDateKey][activePlayer].has(h.key); const btn=document.createElement("button");btn.className="hour-btn"; btn.style.borderColor=active?period.color:"#334155";btn.style.background=active?period.color+"33":"#0f172a";btn.style.color=active?period.color:"#475569"; btn.textContent=h.label; btn.onclick=()=>{const s=availability[selectedDateKey][activePlayer];s.has(h.key)?s.delete(h.key):s.add(h.key);saveAvail();renderCalendar();renderTimePicker();}; container.appendChild(btn); }); }); } function renderAll(){renderWeekNav();renderPlayerGrid();renderCalendar();renderTimePicker();} // โ”€โ”€ BEST TIMES โ”€โ”€ function renderSummary(){ const days=getWindowDays(summaryWeekOffset); document.getElementById("summary-week-label").textContent=getWindowLabel(summaryWeekOffset); document.getElementById("summary-week-sub").textContent=getWindowSub(summaryWeekOffset); const bl=document.getElementById("best-list");bl.innerHTML=""; const results=[]; days.forEach(d=>{ const k=dateKey(d); HOURS.forEach(h=>{ const who=PLAYERS.filter(p=>availability[k]?.[p]?.has(h.key)); if(who.length>=2)results.push({d,slot:h.key,label:h.label,count:who.length,who}); }); }); results.sort((a,b)=>b.count-a.count); if(!results.length){ // Show how many slots each player has for debugging const totalSlots=days.reduce((sum,d)=>{ const k=dateKey(d); return sum+PLAYERS.reduce((ps,p)=>ps+(availability[k]?.[p]?.size||0),0); },0); bl.innerHTML=`
โณ

No overlaps yet for this period!

${totalSlots} total slots marked across all players

`; return; } results.slice(0,10).forEach(({d,label,count,who},idx)=>{ const isTop=idx===0,sc=getHourColor(results[idx].slot); const whoTags=who.map(w=>`${w}`).join(""); bl.innerHTML+=`
${DAY_NAMES[d.getDay()]}
${d.getDate()}
${MONTH_NAMES[d.getMonth()]}
${label}
${whoTags}
${count}
`; }); } // โ”€โ”€ BOOKINGS โ”€โ”€ function renderPayGrid(){ const grid=document.getElementById("b-pay-grid");grid.innerHTML=""; PLAYERS.forEach((p,i)=>{ const isPaying=bookingPayStatus[p]!==false; // default true const row=document.createElement("div");row.className="pay-toggle-row"; row.innerHTML=`${p}
`; grid.appendChild(row); }); } function setPayStatus(p,val){bookingPayStatus[p]=val;renderPayGrid();} function openBookingModal(){ bookingPayStatus={};PLAYERS.forEach(p=>bookingPayStatus[p]=true); document.getElementById("b-date").value=new Date().toISOString().split('T')[0]; document.getElementById("b-court").value=""; document.getElementById("b-time").value=""; document.getElementById("b-cost").value=""; document.getElementById("b-notes").value=""; renderPayGrid(); document.getElementById("booking-modal").classList.add("open"); } function closeBookingModal(){document.getElementById("booking-modal").classList.remove("open");} function saveBooking(){ const court=document.getElementById("b-court").value.trim(); const date=document.getElementById("b-date").value; const time=document.getElementById("b-time").value.trim(); const cost=parseFloat(document.getElementById("b-cost").value)||0; const notes=document.getElementById("b-notes").value.trim(); const payingPlayers=PLAYERS.filter(p=>bookingPayStatus[p]!==false); if(!court||!date){alert("Please fill in court and date.");return;} const id=uid(); db.ref("bookings/"+id).set({id,court,date,time,cost,notes,payStatus:{...bookingPayStatus},createdAt:Date.now()}); closeBookingModal(); } function deleteBooking(id){if(confirm("Delete this booking?"))db.ref("bookings/"+id).remove();} function renderBookings(){ const list=document.getElementById("bookings-list");if(!list)return; const items=Object.values(bookings).sort((a,b)=>a.date.localeCompare(b.date)); if(!items.length){list.innerHTML=`
๐Ÿ“‹

No bookings yet!

`;return;} list.innerHTML=""; items.forEach(b=>{ const d=parseKey(b.date); const dayLabel=`${DAY_NAMES[d.getDay()]}, ${FULL_MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; const payingPlayers=PLAYERS.filter(p=>b.payStatus?b.payStatus[p]!==false:true); const splitAmt=payingPlayers.length>0?(b.cost/payingPlayers.length):0; const splitRows=PLAYERS.map(p=>{ const pays=b.payStatus?b.payStatus[p]!==false:true; const ci=PLAYERS.indexOf(p); return `
${p}${pays?"โ‚ฑ"+splitAmt.toFixed(2):"Free"}
`; }).join(""); list.innerHTML+=`
${dayLabel}
๐ŸŸ ${b.court}
${b.time?`
โฐ ${b.time}
`:""} ${b.notes?`
๐Ÿ“ ${b.notes}
`:""}
๐Ÿ’ฐ Cost Split
${splitRows}
Total${formatPHP(b.cost)}
Each paying playerโ‚ฑ${splitAmt.toFixed(2)}
`; }); } // โ”€โ”€ OPEN PLAY โ”€โ”€ function populateOpPlayers(){ const c=document.getElementById("op-players");c.innerHTML=""; PLAYERS.forEach((p,i)=>{ const isOn=opSelectedPlayers.has(p); const btn=document.createElement("button");btn.className="player-check"; btn.style.borderColor=isOn?PLAYER_COLORS[i]:"#334155";btn.style.background=isOn?PLAYER_COLORS[i]+"22":"#0f172a";btn.style.color=isOn?PLAYER_COLORS[i]:"#64748b"; btn.textContent=p; btn.onclick=()=>{isOn?opSelectedPlayers.delete(p):opSelectedPlayers.add(p);populateOpPlayers();}; c.appendChild(btn); }); } function openOpenPlayModal(){ opSelectedPlayers=new Set(); document.getElementById("op-date").value=new Date().toISOString().split('T')[0]; document.getElementById("op-court").value="";document.getElementById("op-time").value="";document.getElementById("op-notes").value=""; populateOpPlayers(); document.getElementById("openplay-modal").classList.add("open"); } function closeOpenPlayModal(){document.getElementById("openplay-modal").classList.remove("open");} function saveOpenPlay(){ const court=document.getElementById("op-court").value.trim(); const date=document.getElementById("op-date").value; const time=document.getElementById("op-time").value.trim(); const notes=document.getElementById("op-notes").value.trim(); const players=[...opSelectedPlayers]; if(!court||!date||!players.length){alert("Please fill in venue, date, and select players.");return;} const id=uid(); db.ref("openplays/"+id).set({id,court,date,time,notes,players,createdAt:Date.now()}); closeOpenPlayModal(); } function deleteOpenPlay(id){if(confirm("Delete this session?"))db.ref("openplays/"+id).remove();} let editingOpId=null,eopSelectedPlayers=new Set(); function populateEopPlayers(){ const c=document.getElementById("eop-players");if(!c)return;c.innerHTML=""; PLAYERS.forEach((p,i)=>{ const isOn=eopSelectedPlayers.has(p); const btn=document.createElement("button");btn.className="player-check"; btn.style.borderColor=isOn?PLAYER_COLORS[i]:"#334155";btn.style.background=isOn?PLAYER_COLORS[i]+"22":"#0f172a";btn.style.color=isOn?PLAYER_COLORS[i]:"#64748b"; btn.textContent=p; btn.onclick=()=>{if(eopSelectedPlayers.has(p))eopSelectedPlayers.delete(p);else eopSelectedPlayers.add(p);populateEopPlayers();}; c.appendChild(btn); }); } function openEditOpenPlayModal(id){ const op=openPlays[id];if(!op)return; editingOpId=id;eopSelectedPlayers=new Set(op.players||[]); document.getElementById("eop-court").value=op.court||""; document.getElementById("eop-date").value=op.date||""; document.getElementById("eop-time").value=op.time||""; document.getElementById("eop-notes").value=op.notes||""; populateEopPlayers(); document.getElementById("edit-openplay-modal").classList.add("open"); } function closeEditOpenPlayModal(){document.getElementById("edit-openplay-modal").classList.remove("open");editingOpId=null;} function saveEditOpenPlay(){ if(!editingOpId)return; const court=document.getElementById("eop-court").value.trim(); const date=document.getElementById("eop-date").value; const time=document.getElementById("eop-time").value.trim(); const notes=document.getElementById("eop-notes").value.trim(); const players=[...eopSelectedPlayers]; if(!court||!date){alert("Please fill in venue and date.");return;} db.ref("openplays/"+editingOpId).update({court,date,time,notes,players}); closeEditOpenPlayModal(); } function renderOpenPlays(){ const list=document.getElementById("openplay-list");if(!list)return; const items=Object.values(openPlays).sort((a,b)=>b.createdAt-a.createdAt); if(!items.length){list.innerHTML=`
๐ŸŽพ

No open play sessions yet!

`;return;} list.innerHTML=""; items.forEach(op=>{ const d=parseKey(op.date); const dayLabel=`${DAY_NAMES[d.getDay()]}, ${FULL_MONTHS[d.getMonth()]} ${d.getDate()}`; const playerTags=(op.players||[]).map(p=>`${p}`).join(""); list.innerHTML+=`
๐ŸŽพ ${op.court}
${dayLabel}${op.time?" ยท "+op.time:""}
${op.notes?`
๐Ÿ“ ${op.notes}
`:""}
Players
${playerTags||'No players yet โ€” tap Edit to add'}
`; }); } if('serviceWorker' in navigator)navigator.serviceWorker.register('/sw.js').catch(()=>{});