From db8b5f24dc0814f03fd32f83e10b135ebf9caa1c Mon Sep 17 00:00:00 2001 From: Taran Nathan Date: Sat, 26 Sep 2026 21:45:46 -0700 Subject: [PATCH] task view and fix defcon parsing --- client/src/client.gleam | 215 +++++++++++++++++++++++++++-- server/src/server.gleam | 2 + server/static/client.js | 292 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 487 insertions(+), 22 deletions(-) diff --git a/client/src/client.gleam b/client/src/client.gleam index 4b8be57..ba32883 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -77,12 +77,15 @@ pub type Message { Nothing ArchiveProject(Int) UnarchiveProject(Int) + ListView + DashboardView } fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { let page = case current_path() { "/login" -> "login" "/register" -> "register" + "/tasks" -> "tasks" _ -> "dashboard" } @@ -107,7 +110,7 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { ) case page { - "dashboard" | "login" -> #( + "dashboard" | "tasks" | "login" -> #( model, rsvp.get( "/api/dashboard", @@ -121,10 +124,7 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { case message { DashboardLoaded(Ok(value)) -> { - #( - Model(..model, page: "dashboard", dashboard: Some(value), error: None), - effect.none(), - ) + #(Model(..model, dashboard: Some(value), error: None), effect.none()) } DashboardLoaded(Error(_)) -> { #( @@ -214,8 +214,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { field(fields, "due_date"), list.key_find(fields, "defcon") |> result.unwrap("") - |> dynamic.string - |> decode.run(decode.int) + |> int.parse |> result.unwrap(0), ) |> shared.new_task_json @@ -328,6 +327,8 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { rsvp.get("/api/logout", rsvp.expect_ok_response(fn(_) { Nothing })), ) Nothing -> #(model, effect.none()) + ListView -> #(Model(..model, page: "tasks"), effect.none()) + DashboardView -> #(Model(..model, page: "dashboard"), effect.none()) } } @@ -453,6 +454,7 @@ fn view(model: Model) -> Element(Message) { "Create account", RegisterSubmitted, ) + "tasks" -> task_list_view(model) _ -> dashboard_view(model) } } @@ -561,8 +563,9 @@ fn dashboard_view(model: Model) -> Element(Message) { html.text("FRC 972 · IRON CLAW"), html.h1([], [html.text("project board")]), ]), - html.button([event.on_click(LogOut)], [ - html.text("Sign out"), + html.nav([], [ + html.button([event.on_click(ListView)], [html.text("List view")]), + html.button([event.on_click(LogOut)], [html.text("Sign out")]), ]), ]), html.section([attribute.class("hero")], [ @@ -622,6 +625,198 @@ fn dashboard_view(model: Model) -> Element(Message) { } } +fn task_list_view(model: Model) -> Element(Message) { + case model.dashboard { + None -> + html.main([attribute.class("auth")], [ + styles(), + html.h1([], [html.text("Loading Iron Claw")]), + ]) + Some(board) -> + html.main([attribute.class("page")], [ + styles(), + html.header([], [ + html.div([], [ + html.text("FRC 972 · IRON CLAW"), + html.h1([], [html.text("task list")]), + ]), + html.nav([], [ + html.button([event.on_click(DashboardView)], [ + html.text("Board view"), + ]), + html.button([event.on_click(LogOut)], [html.text("Sign out")]), + ]), + ]), + html.section([attribute.class("panel")], [ + html.h2([], [html.text("Find tasks")]), + html.input([ + attribute.class("task-search"), + attribute.styles([ + #("width", "100%"), + #("box-sizing", "border-box"), + ]), + attribute.placeholder("Search owner or assignees"), + event.on_input(TaskSearchChanged), + ]), + html.select( + [ + attribute.class("subteam-filter"), + attribute.styles([ + #("width", "100%"), + #("margin-top", "8px"), + #("box-sizing", "border-box"), + #("padding", "12px 13px"), + #("border", "1px solid #d9cfc2"), + #("border-radius", "0"), + #("background", "#fffcf8"), + #("color", "#28231e"), + #("font", "inherit"), + #("appearance", "auto"), + ]), + event.on_change(SubteamFilterChanged), + ], + [ + html.option([attribute.value("all")], "All subteams"), + html.option([attribute.value("none")], "No subteam"), + html.option([attribute.value("electrical")], "Electrical"), + html.option([attribute.value("software")], "Software"), + html.option([attribute.value("CAD")], "CAD"), + html.option([attribute.value("machining")], "Machining"), + html.option([attribute.value("operations")], "Operations"), + ], + ), + ]), + html.section( + [ + attribute.class("panel table-panel"), + attribute.styles([#("padding", "0")]), + ], + [ + html.div( + [ + attribute.class("task-table-scroll"), + attribute.styles([#("overflow-x", "auto")]), + ], + [ + html.table( + [ + attribute.styles([ + #("width", "100%"), + #("border-collapse", "collapse"), + #("min-width", "760px"), + ]), + ], + [ + html.thead([], [ + html.tr([], [ + table_heading("Task"), + table_heading("Project"), + table_heading("Owner"), + table_heading("Assignees"), + table_heading("Subteams"), + table_heading("Due"), + table_heading("DEFCON"), + table_heading("Status"), + ]), + ]), + html.tbody( + [], + list.map( + task_rows(board.projects, model) + |> list.sort(fn(a, b) { + case int.compare(b.1.defcon, a.1.defcon) { + order.Eq -> + string.compare(a.1.due_date, b.1.due_date) + x -> x + } + }), + task_row, + ), + ), + ], + ), + ], + ), + ], + ), + ]) + } +} + +fn table_heading(label: String) -> Element(Message) { + html.th( + [ + attribute.styles([ + #("padding", "13px 15px"), + #("background", "#f0ebe3"), + #("border-bottom", "1px solid #d9cfc2"), + #("color", "#796f64"), + #("font-size", ".72rem"), + #("letter-spacing", ".06em"), + #("text-align", "left"), + #("text-transform", "uppercase"), + #("white-space", "nowrap"), + ]), + ], + [html.text(label)], + ) +} + +fn task_rows( + projects: List(shared.Project), + model: Model, +) -> List(#(String, shared.Task)) { + projects + |> list.flat_map(fn(project) { + project.tasks + |> list.filter(fn(task) { task_matches(model, task) }) + |> list.map(fn(task) { #(project.name, task) }) + }) +} + +fn task_row(row: #(String, shared.Task)) -> Element(Message) { + let #(project_name, task) = row + html.tr([], [ + table_cell([html.strong([], [html.text(task.title)])]), + table_cell([html.text(project_name)]), + table_cell([html.text(if_blank(task.assignee, "Unassigned"))]), + table_cell([html.text(if_blank(task.assignees, "None"))]), + table_cell([html.text(if_blank(join_subteams(task.subteams), "None"))]), + table_cell([ + html.text(case task.due_date { + "" -> "No due date" + date -> date + }), + ]), + table_cell([ + html.span([attribute.class("task-status")], [ + html.text(int.to_string(task.defcon)), + ]), + ]), + table_cell([ + html.span([attribute.class("task-status")], [ + html.text(task.status), + ]), + ]), + ]) +} + +fn table_cell(children: List(Element(Message))) -> Element(Message) { + html.td( + [ + attribute.styles([ + #("padding", "13px 15px"), + #("border-bottom", "1px solid #eee6dc"), + #("color", "#4e463e"), + #("font-size", ".82rem"), + #("text-align", "left"), + #("vertical-align", "top"), + ]), + ], + children, + ) +} + fn project_sections( model: Model, projects: List(shared.Project), @@ -754,7 +949,7 @@ fn kanban_column(title: String, tasks: List(shared.Task)) -> Element(Message) { ], tasks |> list.sort(fn(a, b) { - case int.compare(a.defcon, b.defcon) { + case int.compare(b.defcon, a.defcon) { order.Eq -> string.compare(a.due_date, b.due_date) a -> a } diff --git a/server/src/server.gleam b/server/src/server.gleam index 4014b49..fc51934 100644 --- a/server/src/server.gleam +++ b/server/src/server.gleam @@ -76,6 +76,8 @@ fn handle_request(db: Database, request: Request) -> Response { http.Get, [] -> shell() http.Get, ["login"] -> shell() http.Get, ["register"] -> shell() + http.Get, ["tasks"] -> shell() + http.Get, ["dashboard"] -> shell() http.Get, ["api", "dashboard"] -> api_dashboard(db, req) http.Get, ["api", "logout"] -> logout(req) http.Post, ["api", "login"] -> diff --git a/server/static/client.js b/server/static/client.js index f92bb97..5b06f97 100644 --- a/server/static/client.js +++ b/server/static/client.js @@ -917,6 +917,26 @@ function append(first, second) { function prepend2(list, item) { return prepend(item, list); } +function flatten_loop(loop$lists, loop$acc) { + while (true) { + let lists = loop$lists; + let acc = loop$acc; + if (lists instanceof Empty) { + return reverse(acc); + } else { + let list = lists.head; + let further_lists = lists.tail; + loop$lists = further_lists; + loop$acc = reverse_and_prepend(list, acc); + } + } +} +function flatten(lists) { + return flatten_loop(lists, List$Empty$const); +} +function flat_map(list, fun) { + return flatten(map2(list, fun)); +} function fold2(loop$list, loop$initial, loop$fun) { while (true) { let list = loop$list; @@ -1544,9 +1564,17 @@ function decode_bool(data) { } // build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs +var Nil = undefined; function identity(x) { return x; } +function parse_int(value) { + if (/^[-+]?(\d+)$/.test(value)) { + return Result$Ok(parseInt(value)); + } else { + return Result$Error(Nil); + } +} function to_string(term) { return term.toString(); } @@ -1898,6 +1926,21 @@ function isResult(data) { return Result$isOk(data) || Result$isError(data); } +// build/dev/javascript/gleam_stdlib/gleam/int.mjs +function compare2(a, b) { + let $ = a === b; + if ($) { + return Order$Eq$const; + } else { + let $1 = a < b; + if ($1) { + return Order$Lt$const; + } else { + return Order$Gt$const; + } + } +} + // build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs class All extends CustomType { } @@ -1911,7 +1954,7 @@ var Direction$Leading$const = new Leading; class Trailing extends CustomType { } var Direction$Trailing$const = new Trailing; -function compare2(a, b) { +function compare3(a, b) { let $ = a === b; if ($) { return Order$Eq$const; @@ -2283,7 +2326,7 @@ function singleton_list(item) { var GT = /* @__PURE__ */ Order$Gt(); var LT = /* @__PURE__ */ Order$Lt(); var EQ = /* @__PURE__ */ Order$Eq(); -function compare3(a, b) { +function compare4(a, b) { if (a.name === b.name) { return EQ; } else if (a.name < b.name) { @@ -2478,7 +2521,7 @@ function prepare(attributes) { } else { let _pipe = attributes; let _pipe$1 = sort(_pipe, (a, b) => { - return compare3(b, a); + return compare4(b, a); }); return merge(_pipe$1, empty_list); } @@ -2886,6 +2929,9 @@ function h3(attrs, children) { function main(attrs, children) { return element2("main", attrs, children); } +function nav(attrs, children) { + return element2("nav", attrs, children); +} function section(attrs, children) { return element2("section", attrs, children); } @@ -2907,6 +2953,24 @@ function span(attrs, children) { function strong(attrs, children) { return element2("strong", attrs, children); } +function table(attrs, children) { + return element2("table", attrs, children); +} +function tbody(attrs, children) { + return element2("tbody", attrs, children); +} +function td(attrs, children) { + return element2("td", attrs, children); +} +function th(attrs, children) { + return element2("th", attrs, children); +} +function thead(attrs, children) { + return element2("thead", attrs, children); +} +function tr(attrs, children) { + return element2("tr", attrs, children); +} function button(attrs, children) { return element2("button", attrs, children); } @@ -3971,7 +4035,7 @@ function diff_attributes(loop$controlled, loop$path, loop$events, loop$old, loop let remaining_old = old.tail; let next = new$5.head; let remaining_new = new$5.tail; - let $ = compare3(prev, next); + let $ = compare4(prev, next); if ($ instanceof Lt) { if (prev instanceof Event2) { let name2 = prev.name; @@ -7438,6 +7502,12 @@ class UnarchiveProject extends CustomType { this[0] = $0; } } +class ListView extends CustomType { +} +var Message$ListView$const = new ListView; +class DashboardView extends CustomType { +} +var Message$DashboardView$const = new DashboardView; function subteam_checkbox(selected, value3, label2) { return label(toList([ class$("subteam-option"), @@ -7590,7 +7660,12 @@ function kanban_column(title2, tasks) { ]), (() => { let _pipe = tasks; let _pipe$1 = sort(_pipe, (a2, b) => { - return compare2(a2.due_date, b.due_date); + let $ = compare2(b.defcon, a2.defcon); + if ($ instanceof Eq) { + return compare3(a2.due_date, b.due_date); + } else { + return $; + } }); return map2(_pipe$1, task_view); })())); @@ -7732,7 +7807,10 @@ function dashboard_view(model) { text3("FRC 972 · IRON CLAW"), h1(List$Empty$const, toList([text3("project board")])) ])), - button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) + nav(List$Empty$const, toList([ + button(toList([on_click(Message$ListView$const)]), toList([text3("List view")])), + button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) + ])) ])), section(toList([class$("hero")]), toList([text3("wassup")])), section(toList([class$("panel")]), toList([ @@ -7792,6 +7870,176 @@ function dashboard_view(model) { ])); } } +function table_cell(children) { + return td(toList([ + styles(toList([ + ["padding", "13px 15px"], + ["border-bottom", "1px solid #eee6dc"], + ["color", "#4e463e"], + ["font-size", ".82rem"], + ["text-align", "left"], + ["vertical-align", "top"] + ])) + ]), children); +} +function task_row(row) { + let project_name = row[0]; + let task = row[1]; + return tr(List$Empty$const, toList([ + table_cell(toList([ + strong(List$Empty$const, toList([text3(task.title)])) + ])), + table_cell(toList([text3(project_name)])), + table_cell(toList([text3(if_blank(task.assignee, "Unassigned"))])), + table_cell(toList([text3(if_blank(task.assignees, "None"))])), + table_cell(toList([text3(if_blank(join_subteams(task.subteams), "None"))])), + table_cell(toList([ + text3((() => { + let $ = task.due_date; + if ($ === "") { + return "No due date"; + } else { + return $; + } + })()) + ])), + table_cell(toList([ + span(toList([class$("task-status")]), toList([text3(to_string(task.defcon))])) + ])), + table_cell(toList([ + span(toList([class$("task-status")]), toList([text3(task.status)])) + ])) + ])); +} +function task_rows(projects, model) { + let _pipe = projects; + return flat_map(_pipe, (project) => { + let _pipe$1 = project.tasks; + let _pipe$2 = filter(_pipe$1, (task) => { + return task_matches(model, task); + }); + return map2(_pipe$2, (task) => { + return [project.name, task]; + }); + }); +} +function table_heading(label2) { + return th(toList([ + styles(toList([ + ["padding", "13px 15px"], + ["background", "#f0ebe3"], + ["border-bottom", "1px solid #d9cfc2"], + ["color", "#796f64"], + ["font-size", ".72rem"], + ["letter-spacing", ".06em"], + ["text-align", "left"], + ["text-transform", "uppercase"], + ["white-space", "nowrap"] + ])) + ]), toList([text3(label2)])); +} +function task_list_view(model) { + let $ = model.dashboard; + if ($ instanceof Some) { + let board = $[0]; + return main(toList([class$("page")]), toList([ + styles2(), + header(List$Empty$const, toList([ + div(List$Empty$const, toList([ + text3("FRC 972 · IRON CLAW"), + h1(List$Empty$const, toList([text3("task list")])) + ])), + nav(List$Empty$const, toList([ + button(toList([on_click(Message$DashboardView$const)]), toList([text3("Board view")])), + button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) + ])) + ])), + section(toList([class$("panel")]), toList([ + h2(List$Empty$const, toList([text3("Find tasks")])), + input(toList([ + class$("task-search"), + styles(toList([["width", "100%"], ["box-sizing", "border-box"]])), + placeholder("Search owner or assignees"), + on_input((var0) => { + return new TaskSearchChanged(var0); + }) + ])), + select(toList([ + class$("subteam-filter"), + styles(toList([ + ["width", "100%"], + ["margin-top", "8px"], + ["box-sizing", "border-box"], + ["padding", "12px 13px"], + ["border", "1px solid #d9cfc2"], + ["border-radius", "0"], + ["background", "#fffcf8"], + ["color", "#28231e"], + ["font", "inherit"], + ["appearance", "auto"] + ])), + on_change((var0) => { + return new SubteamFilterChanged(var0); + }) + ]), toList([ + option(toList([value("all")]), "All subteams"), + option(toList([value("none")]), "No subteam"), + option(toList([value("electrical")]), "Electrical"), + option(toList([value("software")]), "Software"), + option(toList([value("CAD")]), "CAD"), + option(toList([value("machining")]), "Machining"), + option(toList([value("operations")]), "Operations") + ])) + ])), + section(toList([ + class$("panel table-panel"), + styles(toList([["padding", "0"]])) + ]), toList([ + div(toList([ + class$("task-table-scroll"), + styles(toList([["overflow-x", "auto"]])) + ]), toList([ + table(toList([ + styles(toList([ + ["width", "100%"], + ["border-collapse", "collapse"], + ["min-width", "760px"] + ])) + ]), toList([ + thead(List$Empty$const, toList([ + tr(List$Empty$const, toList([ + table_heading("Task"), + table_heading("Project"), + table_heading("Owner"), + table_heading("Assignees"), + table_heading("Subteams"), + table_heading("Due"), + table_heading("DEFCON"), + table_heading("Status") + ])) + ])), + tbody(List$Empty$const, map2((() => { + let _pipe = task_rows(board.projects, model); + return sort(_pipe, (a2, b) => { + let $1 = compare2(b[1].defcon, a2[1].defcon); + if ($1 instanceof Eq) { + return compare3(a2[1].due_date, b[1].due_date); + } else { + return $1; + } + }); + })(), task_row)) + ])) + ])) + ])) + ])); + } else { + return main(toList([class$("auth")]), toList([ + styles2(), + h1(List$Empty$const, toList([text3("Loading Iron Claw")])) + ])); + } +} function creat_account_view(model, title2, submit, on_submit2) { let _block; let $ = submit === "Log in"; @@ -7900,6 +8148,8 @@ function view(model) { return creat_account_view(model, "Create account", "Create account", (var0) => { return new RegisterSubmitted(var0); }); + } else if ($ === "tasks") { + return task_list_view(model); } else { return dashboard_view(model); } @@ -8003,7 +8253,7 @@ function update2(model, message2) { if ($ instanceof Ok) { let value3 = $[0]; return [ - new Model("dashboard", new Some(value3), Option$None$const, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + new Model(model.page, new Some(value3), Option$None$const, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else { @@ -8042,9 +8292,8 @@ function update2(model, message2) { let _pipe = new NewTask(field2(fields, "title"), field2(fields, "description"), field2(fields, "assignee"), field2(fields, "assignees"), model.task_subteams, field2(fields, "due_date"), (() => { let _pipe2 = key_find(fields, "defcon"); let _pipe$1 = unwrap2(_pipe2, ""); - let _pipe$2 = identity(_pipe$1); - let _pipe$3 = run(_pipe$2, int2); - return unwrap2(_pipe$3, 0); + let _pipe$2 = parse_int(_pipe$1); + return unwrap2(_pipe$2, 0); })()); _block = new_task_json(_pipe); let body2 = _block; @@ -8235,7 +8484,7 @@ function update2(model, message2) { })()); })) ]; - } else { + } else if (message2 instanceof UnarchiveProject) { let project_id = message2[0]; return [ model, @@ -8248,6 +8497,16 @@ function update2(model, message2) { })()); })) ]; + } else if (message2 instanceof ListView) { + return [ + new Model("tasks", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + none() + ]; + } else { + return [ + new Model("dashboard", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + none() + ]; } } function current_path() { @@ -8262,6 +8521,8 @@ function init(_) { _block = "login"; } else if ($ === "/register") { _block = "register"; + } else if ($ === "/tasks") { + _block = "tasks"; } else { _block = "dashboard"; } @@ -8274,6 +8535,13 @@ function init(_) { return new DashboardLoaded(var0); })) ]; + } else if (page === "tasks") { + return [ + model, + get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { + return new DashboardLoaded(var0); + })) + ]; } else if (page === "login") { return [ model, @@ -8289,7 +8557,7 @@ function main2() { let app = application(init, update2, view); let $ = start4(app, "#app", Option$None$const); if (!($ instanceof Ok)) { - throw makeError("let_assert", FILEPATH, "client", 22, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 508, end: 558, pattern_start: 519, pattern_end: 524 }); + throw makeError("let_assert", FILEPATH, "client", 23, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 527, end: 577, pattern_start: 538, pattern_end: 543 }); } return; } -- 2.47.3