work-graph-simple/app.js
2025-11-03 15:58:48 -05:00

337 lines
10 KiB
JavaScript

/** @import * as TYPES from "./graph/types.ts" */
import * as FSHandle from "./store-directory-handle.js";
import Styles from "./styles.js";
const {DOM, Div, Tag} = Styles;
async function PickHandle()
{
handle = await showDirectoryPicker();
await FSHandle.setDirectoryHandle(handle);
await LoadHandleFiles();
}
async function LoadHandleFiles()
{
console.log("fetching room.js", handle);
if(handle)
{
try
{
const module = await import("./graph/room.js"+"?bust="+Math.random());
/** @type {Record<string, TYPES.GraphParts>} */
const read = module.default();
for(const roomKey in read)
{
const room = read[roomKey]
for(const pass in room.Pass)
{
await room.Pass[pass].load();
}
}
rooms.val = read;
}
catch(e)
{
console.log("the handle exists, but the request failed. the service work must not be ready yet.", e)
rooms.val = {};
}
}
else
{
console.log("no fs handle has been set, cannot get room graph")
rooms.val = {};
}
}
/** @type {Van.State<Record<string, TYPES.GraphParts>>} */
const rooms = van.state({});
let handle = await FSHandle.getDirectoryHandle();
await LoadHandleFiles();
/** @type {Van.State<TYPES.User|false>} */
const loggedIn = van.state(false);
const blocking = van.state(false);
const showDesks = van.state(true, "desks");
function Input(handler=(str)=>{})
{
const input = DOM.textarea({style:"vertical-align:text-top; width:500px; height:200px;"});
let submitButton = Div.Plain(DOM.button({onclick(){
handler(input.value);
input.value = "";
mountPoint.remove();
}}, "submit value"));
let cancelButton = DOM.button({onclick(){
input.value = "";
mountPoint.remove();
}}, "cancel")
const mountPoint = Div.Plain({onclick(e){e.stopPropagation();}}, input, cancelButton, submitButton);
return mountPoint;
}
/** @type {(inParts:Record<string, TYPES.Part>, inPasses:Record<string, TYPES.Pass>)=>HTMLElement} */
function Parts(inParts, inPasses)
{
const rows = [];
const row = [DOM.th()]
for(const pass in inPasses)
{
row.push(DOM.th(inPasses[pass].name));
}
rows.push(DOM.thead(row));
Object.entries(inParts).map(([part_id, part])=>{
const row = [DOM.th(part.name)];
for(const [pass, data] of part.pass)
{
row.push(DOM.td.Part(data.work.map(w=>Div.Plain(w[1]) )))
}
rows.push(DOM.tr(row))
});
return DOM.table.GapVertical(rows);
}
const deskRender = van.state(0);
/** @type {(inDesks:Record<string, TYPES.Desk>)=>HTMLElement} */
function Desks(inDesks)
{
return Div.PartGroup(
Object.entries(inDesks).map(([desk_id, desk])=>{
loggedIn.val;
deskRender.val;
console.log("reredering desk", desk.name);
if (loggedIn.val)
{
let userInRole = false;
for(const role of desk.role)
{
if(role.user.includes(loggedIn.val))
{
userInRole = true;
}
}
if(!userInRole)
{
return null;
}
}
const work = [];
for(const [pass, scan] of desk.pass)
{
// at least one but not all need fields are empty
const caution = scan.need_empty.length>0 && scan.need_empty.length<desk.need.length;
work.push(DOM.tr(
DOM.td(pass.name),
desk.need.map((part, index, array)=>
{
const partPass = part.pass.get(pass);
if(!partPass){ return null }
const latest = partPass.work.find(t=>t[0] == partPass.time)?.[1];
const attributes = {};
if(latest)
{
attributes.class = Tag("PartGood")
}
else
{
attributes.class = caution ? Tag("PartCaution") : Tag("PartEmpty")
}
if(scan.need_dirty.includes(index))
{
attributes.class = Tag("PartDirty")
}
return DOM.td(
Div.Part(
attributes,
latest
)
);
}),
DOM.td(Div.Icon("⇉")),
desk.make.map((part, index, array)=>
{
const partPass = part.pass.get(pass);
if(!partPass){ return null }
const latest = partPass.work.find(t=>t[0] == partPass.time)?.[1];
const attributes = {
onclick(){
loggedIn.rawVal && van.add(this, Input((str)=>{
if(loggedIn.rawVal)
{
blocking.val = true;
partPass.make(loggedIn.rawVal, str).then(()=>{
deskRender.val++;
blocking.val = false;
})
}
else
{
return false;
}
}));
}
};
if(latest)
{
attributes.class = Tag("PartGood")
}
else
{
attributes.class = Tag("PartEmpty")
}
if( (desk.need.length==0 && !latest) || scan.make_dirty.includes(index))
{
if(!latest && caution)
{
attributes.class = Tag("PartCaution")
}
else
{
attributes.class = Tag("PartDirty");
}
}
return DOM.td(
Div.Part(
attributes,
latest
)
);
}),
))
}
return Div.DeskContainer(
DOM.h3(desk.name),
DOM.table.GapHorizontal(
DOM.thead(
DOM.tr(
DOM.th(),
desk.need.map((part, index)=>DOM.th(part.name)),
DOM.th("→"),
desk.make.map((part, index)=>DOM.th(part.name))
)
),
work
)
)
}),
)
}
/** @type {(room_id:string, graphParts:TYPES.GraphParts)=>HTMLElement} */
function Room(room_id, graphParts)
{
const rerender = van.state(0);
blocking.val;
return Div.Plain(
Div.Plain("Users:"),
Div.PartGroup(
Object.entries(graphParts.User).map(([user_id, user])=>{
return ()=>{
return Div.Part(
DOM.div.Plain(user.name),
()=>{
return DOM.button.Plain(
{onclick(){
loggedIn.val = (loggedIn.val == user) ? false : user;
}},
loggedIn.val == user ? "this is me" : "impersonate"
)
},
)
}
})
),
// Div.Plain("Passes:"),
// Div.PartGroup(
// Object.entries(graphParts.Pass).map(([pass_id, pass])=>{
// return ()=>{
// console.log("rerendering...", rerender.rawVal);
//
// rerender.val;
//
// return Div.Part(
// DOM.div.Plain(pass.name),
// ()=>{
// return DOM.button.Plain(
// {async onclick(){
// await pass[pass.live ? "dump" : "load"]();
// rerender.val++
// }},
// pass.live ? "Dump" : "Load"
// )
// },
// )
// }
// })
// ),
()=>{
return DOM.button({onclick(){
showDesks.val = !showDesks.val;
}}, showDesks.val ? "Show Parts" : "Show Desks")
},
()=>{
rerender.val;
return showDesks.val ? Desks(graphParts.Desk) : Parts(graphParts.Part, graphParts.Pass);
},
()=>{
return blocking.val ? Div.BlockScreen() : null
}
)
}
function App()
{
return Div.Plain(
DOM.button({onclick:PickHandle}, "Pick Directory"),
Object.entries(rooms.val).map(([room_id, graphParts])=>
Room(room_id, graphParts)
)
)
}
van.add(document.body, App);