Files
roast_command_center/public/js/workspace.js
T
2026-09-07 06:38:16 -04:00

97 lines
5.4 KiB
JavaScript

import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
const root = document.getElementById("workspace-content");
const n = (tag, text = "", cls = "") => Object.assign(document.createElement(tag), {textContent:text,className:cls});
const link = (text, href, cls = "") => Object.assign(n("a",text,cls), {href});
const now = new Date();
const initial = new URLSearchParams(location.search).get("month");
let month = /^20\d{2}-(0[1-9]|1[0-2])$/.test(initial || "") ? initial : `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`;
let events = [], generation = 0;
const labels = {roast:"Roasted",cupping:"Cupped",brew:"Brewed",plan:"Plan created"};
const toolbar = n("div","","calendar-toolbar");
const previous = n("button","← Previous","ghost-btn"), next = n("button","Next →","ghost-btn"), current = n("button","This month","ghost-btn");
const heading = n("h2");
const filter = n("select","","field-input");
filter.setAttribute("aria-label","Activity type");
for (const [value,label] of [["","All activity"],...Object.entries(labels)]) filter.append(Object.assign(n("option",label),{value}));
toolbar.append(previous,heading,next,current,filter);
const status = n("p","","muted"); status.setAttribute("role","status");
const content = n("div");
const shortcuts = n("div","","calendar-shortcuts");
shortcuts.append(link("Plan a roast","/plans","primary-btn"),link("Log a roast","/roasts","ghost-btn"),link("Cup coffee","/cupping","ghost-btn"),link("Log a brew","/brews","ghost-btn"));
root.replaceChildren(shortcuts,toolbar,status,content);
function render() {
const selected = events.filter(e=>!filter.value || e.kind===filter.value);
status.textContent = `${selected.length} ${selected.length===1?"activity":"activities"} this month`;
content.replaceChildren();
const [year,m] = month.split("-").map(Number);
heading.textContent = new Date(year,m-1,1).toLocaleDateString(undefined,{month:"long",year:"numeric"});
const grid = n("div","","activity-calendar");
for (const day of ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]) grid.append(n("div",day,"calendar-weekday"));
const offset = new Date(year,m-1,1).getDay(), count = new Date(year,m,0).getDate();
for(let i=0;i<offset;i++) grid.append(n("div","","calendar-day calendar-padding"));
const today = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`;
for(let day=1;day<=count;day++) {
const key = month+"-"+String(day).padStart(2,"0");
const cell = n("section","","calendar-day"+(key===today?" calendar-today":""));
const title = n("h3",String(day));
title.setAttribute("aria-label",new Date(year,m-1,day).toLocaleDateString(undefined,{dateStyle:"full"}));
cell.append(title);
for(const event of selected.filter(e=>e.date===key)) {
const a = link(labels[event.kind]+" · "+event.title,event.href,"calendar-event event-"+event.kind);
a.title = a.textContent;
cell.append(a);
}
grid.append(cell);
}
for(let i=(offset+count)%7;i>0 && i<7;i++) grid.append(n("div","","calendar-day calendar-padding"));
content.append(grid);
const agenda = n("section","","calendar-agenda");
agenda.append(n("h2","Activity log"));
if(!selected.length) agenda.append(n("p","No activity this month. Log a roast, cupping or brew to see it here."));
let lastDate;
for(const event of [...selected].reverse()) {
if(event.date!==lastDate) {
agenda.append(n("h3",new Date(event.date+"T12:00:00").toLocaleDateString(undefined,{weekday:"long",month:"short",day:"numeric"})));
lastDate=event.date;
}
const row = n("div","","calendar-log-row");
row.append(n("span",labels[event.kind],"calendar-kind event-"+event.kind),link(event.title,event.href));
if(event.planHref) row.append(link("View plan",event.planHref,"muted"));
if(event.dateSource==="logged") row.append(n("small","Date logged","muted"));
agenda.append(row);
}
content.append(agenda);
}
async function load() {
const token = ++generation;
filter.disabled = true;
events = [];
status.textContent="Loading activity…";
content.replaceChildren();
const [year,m] = month.split("-").map(Number);
heading.textContent=new Date(year,m-1,1).toLocaleDateString(undefined,{month:"long",year:"numeric"});
previous.disabled=month==="2000-01"; next.disabled=month==="2099-12";
history.replaceState(null,"","/home?month="+month);
try {
const data = await api("/api/activity?"+new URLSearchParams({month,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone}));
if(token!==generation)return;
events=data.events; filter.disabled = false; render();
} catch(error) {
if(token!==generation)return;
status.textContent="Could not load activity. "+error.message;
const retry=n("button","Retry","ghost-btn");retry.onclick=load;content.append(retry);
}
}
function move(delta) {
const [year,m]=month.split("-").map(Number), date=new Date(year,m-1+delta,1);
month=`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,"0")}`; load();
}
previous.onclick=()=>move(-1);next.onclick=()=>move(1);
current.onclick=()=>{month=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`;load();};
filter.onchange=()=>{if(!status.textContent.startsWith("Loading"))render();};
initSideNav();
await loadNavUser();
document.getElementById("btn-logout").onclick=async()=>{await protectedFetch("/api/auth/logout",{method:"POST"});location.assign("/");};
await load();