From: Taran Nathan Date: Mon, 28 Sep 2026 00:37:42 +0000 (-0700) Subject: calendar view X-Git-Url: https://git.taranathan.com/?a=commitdiff_plain;h=refs%2Fheads%2Fmain;p=robotics-task-manager.git calendar view --- diff --git a/client/src/client.gleam b/client/src/client.gleam index a3a415b..f56f2dc 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -1,5 +1,3 @@ -import gleam/dynamic -import gleam/dynamic/decode import gleam/int import gleam/json import gleam/list @@ -15,6 +13,7 @@ import lustre/element/html import lustre/event import plinth/browser/location import plinth/browser/window +import plinth/javascript/date import rsvp import shared @@ -46,6 +45,7 @@ type Model { task_due_date: String, subteam_filter: String, task_search: String, + calendar_week: Int, ) } @@ -79,6 +79,9 @@ pub type Message { UnarchiveProject(Int) ListView DashboardView + CalendarView + PreviousWeek + NextWeek } fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { @@ -86,6 +89,7 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { "/login" -> "login" "/register" -> "register" "/tasks" -> "tasks" + "/calendar" -> "calendar" _ -> "dashboard" } @@ -107,10 +111,11 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { task_due_date: "", subteam_filter: "all", task_search: "", + calendar_week: 0, ) case page { - "dashboard" | "tasks" | "login" -> #( + "dashboard" | "tasks" | "calendar" | "login" -> #( model, rsvp.get( "/api/dashboard", @@ -329,6 +334,15 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { Nothing -> #(model, effect.none()) ListView -> #(Model(..model, page: "tasks"), effect.none()) DashboardView -> #(Model(..model, page: "dashboard"), effect.none()) + CalendarView -> #(Model(..model, page: "calendar"), effect.none()) + PreviousWeek -> #( + Model(..model, calendar_week: model.calendar_week - 1), + effect.none(), + ) + NextWeek -> #( + Model(..model, calendar_week: model.calendar_week + 1), + effect.none(), + ) } } @@ -455,6 +469,7 @@ fn view(model: Model) -> Element(Message) { RegisterSubmitted, ) "tasks" -> task_list_view(model) + "calendar" -> calendar_view(model) _ -> dashboard_view(model) } } @@ -565,6 +580,9 @@ fn dashboard_view(model: Model) -> Element(Message) { ]), html.nav([], [ html.button([event.on_click(ListView)], [html.text("List view")]), + html.button([event.on_click(CalendarView)], [ + html.text("Calendar"), + ]), html.button([event.on_click(LogOut)], [html.text("Sign out")]), ]), ]), @@ -644,6 +662,7 @@ fn task_list_view(model: Model) -> Element(Message) { html.button([event.on_click(DashboardView)], [ html.text("Board view"), ]), + html.button([event.on_click(CalendarView)], [html.text("Calendar")]), html.button([event.on_click(LogOut)], [html.text("Sign out")]), ]), ]), @@ -817,6 +836,313 @@ fn table_cell(children: List(Element(Message))) -> Element(Message) { ) } +fn calendar_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("calendar")]), + ]), + html.nav([], [ + html.button([event.on_click(DashboardView)], [ + html.text("Board view"), + ]), + html.button([event.on_click(ListView)], [html.text("List 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.placeholder("Search owner or assignees"), + event.on_input(TaskSearchChanged), + attribute.styles([ + #("width", "100%"), + #("box-sizing", "border-box"), + ]), + ]), + html.select( + [ + attribute.class("subteam-filter"), + event.on_change(SubteamFilterChanged), + 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"), + ]), + ], + [ + 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"), + attribute.styles([#("padding", "0"), #("overflow", "hidden")]), + ], + [ + html.div( + [ + attribute.styles([ + #("display", "flex"), + #("justify-content", "space-between"), + #("align-items", "center"), + #("padding", "14px 16px"), + #("border-bottom", "1px solid #e2d9cc"), + ]), + ], + [ + html.button([event.on_click(PreviousWeek)], [ + html.text("← Previous week"), + ]), + html.strong([], [html.text(week_label(model.calendar_week))]), + html.button([event.on_click(NextWeek)], [ + html.text("Next week →"), + ]), + ], + ), + html.div( + [ + attribute.class("calendar-grid"), + attribute.styles([ + #("display", "grid"), + #("grid-template-columns", "repeat(7, minmax(150px, 1fr))"), + #("overflow-x", "auto"), + ]), + ], + list.map(current_week_dates(model.calendar_week), fn(x) { + let #(day_no_year, day, day_of_week) = x + calendar_day( + day_no_year, + day_of_week, + calendar_tasks(board.projects, model, day), + ) + }), + ), + ], + ), + ]) + } +} + +fn current_week_dates(week_offset: Int) -> List(#(String, String, String)) { + let today = date.now() + let days_from_monday = case date.day(today) { + 0 -> 6 + day -> day - 1 + } + let current_week_start = + subtract_days( + date.year(today), + date.month(today) + 1, + date.date(today), + days_from_monday, + ) + let start = shift_week(current_week_start, week_offset) + + [0, 1, 2, 3, 4, 5, 6] + |> list.map(fn(offset) { + #( + format_date_no_year(add_days(start, offset)), + format_date(add_days(start, offset)), + offset |> num_to_day, + ) + }) +} + +fn shift_week(date_value: #(Int, Int, Int), weeks: Int) -> #(Int, Int, Int) { + case weeks { + 0 -> date_value + value if value > 0 -> add_days(date_value, value * 7) + value -> subtract_date_days(date_value, 0 - value * 7) + } +} + +fn subtract_date_days( + date_value: #(Int, Int, Int), + days: Int, +) -> #(Int, Int, Int) { + let #(year, month, day) = date_value + subtract_days(year, month, day, days) +} + +fn week_label(week_offset: Int) -> String { + let dates = current_week_dates(week_offset) + let first = list.first(dates) |> result.unwrap(#("", "", "")) + let last = list.last(dates) |> result.unwrap(#("", "", "")) + first.1 <> " – " <> last.1 +} + +fn num_to_day(day: Int) -> String { + case day { + 0 -> "Mon" + 1 -> "Tue" + 2 -> "Wed" + 3 -> "Thu" + 4 -> "Fri" + 5 -> "Sat" + 6 -> "Sun" + _ -> "" + } +} + +fn subtract_days( + year: Int, + month: Int, + day: Int, + count: Int, +) -> #(Int, Int, Int) { + case count { + 0 -> #(year, month, day) + _ -> { + let #(previous_year, previous_month, previous_day) = case day { + 1 -> { + let month = case month { + 1 -> 12 + _ -> month - 1 + } + let year = case month == 12 { + True -> year - 1 + False -> year + } + #(year, month, days_in_month(year, month)) + } + _ -> #(year, month, day - 1) + } + subtract_days(previous_year, previous_month, previous_day, count - 1) + } + } +} + +fn add_days(date_value: #(Int, Int, Int), count: Int) -> #(Int, Int, Int) { + case count { + 0 -> date_value + _ -> { + let #(year, month, day) = date_value + let #(next_year, next_month, next_day) = case + day == days_in_month(year, month) + { + True -> + case month == 12 { + True -> #(year + 1, 1, 1) + False -> #(year, month + 1, 1) + } + False -> #(year, month, day + 1) + } + add_days(#(next_year, next_month, next_day), count - 1) + } + } +} + +fn days_in_month(year: Int, month: Int) -> Int { + case month { + 2 -> { + let divisible_by_400 = int.remainder(year, by: 400) == Ok(0) + let divisible_by_4 = int.remainder(year, by: 4) == Ok(0) + let divisible_by_100 = int.remainder(year, by: 100) == Ok(0) + case divisible_by_400 || divisible_by_4 && !divisible_by_100 { + True -> 29 + False -> 28 + } + } + 4 | 6 | 9 | 11 -> 30 + _ -> 31 + } +} + +fn format_date_no_year(date_value: #(Int, Int, Int)) -> String { + let #(_, month, day) = date_value + two_digits(month) <> "-" <> two_digits(day) +} + +fn format_date(date_value: #(Int, Int, Int)) -> String { + let #(year, month, day) = date_value + int.to_string(year) <> "-" <> two_digits(month) <> "-" <> two_digits(day) +} + +fn two_digits(value: Int) -> String { + let text = int.to_string(value) + case string.length(text) { + 1 -> "0" <> text + _ -> text + } +} + +fn calendar_tasks( + projects: List(shared.Project), + model: Model, + date: String, +) -> List(#(String, shared.Task)) { + task_rows(projects, model) + |> list.filter(fn(row) { row.1.due_date == date }) +} + +fn calendar_day( + date: String, + day_of_week: String, + tasks: List(#(String, shared.Task)), +) -> Element(Message) { + html.div( + [ + attribute.styles([ + #("min-height", "260px"), + #("padding", "14px"), + #("border-right", "1px solid #e2d9cc"), + #("background", "#fff"), + ]), + ], + [ + html.h3([attribute.style("margin-bottom", "1px")], [ + html.text(day_of_week), + ]), + html.h3([attribute.style("margin-top", "0px")], [ + html.text(date), + ]), + html.div( + [], + list.map(tasks, fn(row) { + let #(project, task) = row + html.div([attribute.class("task")], [ + html.strong([], [html.text(task.title)]), + html.small([], [ + html.strong([], [html.text("Project: ")]), + html.text(project), + ]), + html.span([], [ + html.strong([], [ + html.text("DEFCON " <> int.to_string(task.defcon)), + ]), + ]), + ]) + }), + ), + ], + ) +} + fn project_sections( model: Model, projects: List(shared.Project), diff --git a/server/static/client.js b/server/static/client.js index 9b76bce..a51fe1d 100644 --- a/server/static/client.js +++ b/server/static/client.js @@ -277,6 +277,13 @@ function structurallyCompatibleObjects(a, b) { return false; return a.constructor === b.constructor; } +function remainderInt(a, b) { + if (b === 0) { + return 0; + } else { + return a % b; + } +} function makeError(variant, file, module, line, fn, message, extra) { let error = new globalThis.Error(message); error.gleam_error = variant; @@ -823,6 +830,14 @@ function contains(loop$list, loop$elem) { } } } +function first(list) { + if (list instanceof Empty) { + return new Error2(undefined); + } else { + let first$1 = list.head; + return new Ok(first$1); + } +} function filter_loop(loop$list, loop$fun, loop$acc) { while (true) { let list = loop$list; @@ -899,20 +914,20 @@ function map2(list, fun) { } function append_loop(loop$first, loop$second) { while (true) { - let first = loop$first; + let first2 = loop$first; let second = loop$second; - if (first instanceof Empty) { + if (first2 instanceof Empty) { return second; } else { - let first$1 = first.head; - let rest$1 = first.tail; + let first$1 = first2.head; + let rest$1 = first2.tail; loop$first = rest$1; loop$second = prepend(first$1, second); } } } -function append(first, second) { - return append_loop(reverse(first), second); +function append(first2, second) { + return append_loop(reverse(first2), second); } function prepend2(list, item) { return prepend(item, list); @@ -1342,6 +1357,23 @@ function each(loop$list, loop$f) { } } } +function last(loop$list) { + while (true) { + let list = loop$list; + if (list instanceof Empty) { + return new Error2(undefined); + } else { + let $ = list.tail; + if ($ instanceof Empty) { + let last$1 = list.head; + return new Ok(last$1); + } else { + let rest$1 = $; + loop$list = rest$1; + } + } + } +} // build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs class DecodeError extends CustomType { @@ -1426,9 +1458,9 @@ function run_decoders(loop$data, loop$failure, loop$decoders) { } } } -function one_of(first, alternatives) { +function one_of(first2, alternatives) { return new Decoder((dynamic_data) => { - let $ = first.function(dynamic_data); + let $ = first2.function(dynamic_data); let layer = $; let errors = $[1]; if (errors instanceof Empty) { @@ -1578,6 +1610,21 @@ function parse_int(value) { function to_string(term) { return term.toString(); } +function string_length(string4) { + if (string4 === "") { + return 0; + } + const iterator = graphemes_iterator(string4); + if (iterator) { + let i = 0; + for (const _ of iterator) { + i++; + } + return i; + } else { + return string4.match(/./gsu).length; + } +} function graphemes(string4) { const iterator = graphemes_iterator(string4); if (iterator) { @@ -1747,11 +1794,11 @@ class Inspector { } #dict(map4) { let body = "dict.from_list(["; - let first = true; + let first2 = true; body = fold(map4, body, (body2, key, value) => { - if (!first) + if (!first2) body2 = body2 + ", "; - first = false; + first2 = false; return body2 + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")"; }); return body + "])"; @@ -1940,6 +1987,14 @@ function compare2(a, b) { } } } +function remainder(dividend, divisor) { + if (divisor === 0) { + return new Error2(undefined); + } else { + let divisor$1 = divisor; + return new Ok(remainderInt(dividend, divisor$1)); + } +} // build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs class All extends CustomType { @@ -5277,8 +5332,8 @@ class Reconciler { const throttle = throttles.get(type); if (throttle) { const now = Date.now(); - const last = throttle.last || 0; - if (now > last + throttle.delay) { + const last2 = throttle.last || 0; + if (now > last2 + throttle.delay) { throttle.last = now; throttle.lastEvent = event3; this.#dispatch(event3, data); @@ -5986,8 +6041,8 @@ function start4(app, selector, arguments$) { } // build/dev/javascript/gleam_stdlib/gleam/pair.mjs -function new$5(first, second) { - return [first, second]; +function new$5(first2, second) { + return [first2, second]; } // build/dev/javascript/lustre/lustre/event.mjs @@ -6236,6 +6291,23 @@ async function import_(string6) { } } +// build/dev/javascript/plinth/date_ffi.mjs +function now() { + return new Date; +} +function year(d) { + return d.getFullYear(); +} +function month(d) { + return d.getMonth(); +} +function day(d) { + return d.getDay(); +} +function date(d) { + return d.getDate(); +} + // build/dev/javascript/gleam_stdlib/gleam/uri.mjs class Uri extends CustomType { constructor(scheme, userinfo, host, port, path, query, fragment3) { @@ -7322,7 +7394,7 @@ function status_change_json(value3) { var FILEPATH = "src/client.gleam"; class Model extends CustomType { - constructor(page, dashboard, error, username, password, invite_password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_subteams, task_due_date, subteam_filter, task_search) { + constructor(page, dashboard, error, username, password, invite_password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_subteams, task_due_date, subteam_filter, task_search, calendar_week) { super(); this.page = page; this.dashboard = dashboard; @@ -7340,6 +7412,7 @@ class Model extends CustomType { this.task_due_date = task_due_date; this.subteam_filter = subteam_filter; this.task_search = task_search; + this.calendar_week = calendar_week; } } @@ -7508,6 +7581,15 @@ var Message$ListView$const = new ListView; class DashboardView extends CustomType { } var Message$DashboardView$const = new DashboardView; +class CalendarView extends CustomType { +} +var Message$CalendarView$const = new CalendarView; +class PreviousWeek extends CustomType { +} +var Message$PreviousWeek$const = new PreviousWeek; +class NextWeek extends CustomType { +} +var Message$NextWeek$const = new NextWeek; function subteam_checkbox(selected, value3, label2) { return label(toList([ class$("subteam-option"), @@ -7809,6 +7891,7 @@ function dashboard_view(model) { ])), nav(List$Empty$const, toList([ button(toList([on_click(Message$ListView$const)]), toList([text3("List view")])), + button(toList([on_click(Message$CalendarView$const)]), toList([text3("Calendar")])), button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) ])) ])), @@ -7870,6 +7953,336 @@ function dashboard_view(model) { ])); } } +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 calendar_tasks(projects, model, date2) { + let _pipe = task_rows(projects, model); + return filter(_pipe, (row) => { + return row[1].due_date === date2; + }); +} +function calendar_day(date2, day_of_week, tasks) { + return div(toList([ + styles(toList([ + ["min-height", "260px"], + ["padding", "14px"], + ["border-right", "1px solid #e2d9cc"], + ["background", "#fff"] + ])) + ]), toList([ + h3(toList([style("margin-bottom", "1px")]), toList([text3(day_of_week)])), + h3(toList([style("margin-top", "0px")]), toList([text3(date2)])), + div(List$Empty$const, map2(tasks, (row) => { + let project = row[0]; + let task = row[1]; + return div(toList([class$("task")]), toList([ + strong(List$Empty$const, toList([text3(task.title)])), + small(List$Empty$const, toList([ + strong(List$Empty$const, toList([text3("Project: ")])), + text3(project) + ])), + span(List$Empty$const, toList([ + strong(List$Empty$const, toList([ + text3("DEFCON " + to_string(task.defcon)) + ])) + ])) + ])); + })) + ])); +} +function num_to_day(day2) { + if (day2 === 0) { + return "Mon"; + } else if (day2 === 1) { + return "Tue"; + } else if (day2 === 2) { + return "Wed"; + } else if (day2 === 3) { + return "Thu"; + } else if (day2 === 4) { + return "Fri"; + } else if (day2 === 5) { + return "Sat"; + } else if (day2 === 6) { + return "Sun"; + } else { + return ""; + } +} +function days_in_month(year2, month2) { + if (month2 === 2) { + let divisible_by_400 = isEqual(remainder(year2, 400), new Ok(0)); + let divisible_by_4 = isEqual(remainder(year2, 4), new Ok(0)); + let divisible_by_100 = isEqual(remainder(year2, 100), new Ok(0)); + let $ = divisible_by_400 || divisible_by_4 && !divisible_by_100; + if ($) { + return 29; + } else { + return 28; + } + } else if (month2 === 4) { + return 30; + } else if (month2 === 6) { + return 30; + } else if (month2 === 9) { + return 30; + } else if (month2 === 11) { + return 30; + } else { + return 31; + } +} +function add_days(loop$date_value, loop$count) { + while (true) { + let date_value = loop$date_value; + let count = loop$count; + if (count === 0) { + return date_value; + } else { + let year2 = date_value[0]; + let month2 = date_value[1]; + let day2 = date_value[2]; + let _block; + let $1 = day2 === days_in_month(year2, month2); + if ($1) { + let $2 = month2 === 12; + if ($2) { + _block = [year2 + 1, 1, 1]; + } else { + _block = [year2, month2 + 1, 1]; + } + } else { + _block = [year2, month2, day2 + 1]; + } + let $ = _block; + let next_year = $[0]; + let next_month = $[1]; + let next_day = $[2]; + loop$date_value = [next_year, next_month, next_day]; + loop$count = count - 1; + } + } +} +function two_digits(value3) { + let text4 = to_string(value3); + let $ = string_length(text4); + if ($ === 1) { + return "0" + text4; + } else { + return text4; + } +} +function format_date(date_value) { + let year2 = date_value[0]; + let month2 = date_value[1]; + let day2 = date_value[2]; + return to_string(year2) + "-" + two_digits(month2) + "-" + two_digits(day2); +} +function format_date_no_year(date_value) { + let month2 = date_value[1]; + let day2 = date_value[2]; + return two_digits(month2) + "-" + two_digits(day2); +} +function subtract_days(loop$year, loop$month, loop$day, loop$count) { + while (true) { + let year2 = loop$year; + let month2 = loop$month; + let day2 = loop$day; + let count = loop$count; + if (count === 0) { + return [year2, month2, day2]; + } else { + let _block; + if (day2 === 1) { + let _block$1; + if (month2 === 1) { + _block$1 = 12; + } else { + _block$1 = month2 - 1; + } + let month$1 = _block$1; + let _block$2; + let $1 = month$1 === 12; + if ($1) { + _block$2 = year2 - 1; + } else { + _block$2 = year2; + } + let year$1 = _block$2; + _block = [year$1, month$1, days_in_month(year$1, month$1)]; + } else { + _block = [year2, month2, day2 - 1]; + } + let $ = _block; + let previous_year = $[0]; + let previous_month = $[1]; + let previous_day = $[2]; + loop$year = previous_year; + loop$month = previous_month; + loop$day = previous_day; + loop$count = count - 1; + } + } +} +function subtract_date_days(date_value, days) { + let year2 = date_value[0]; + let month2 = date_value[1]; + let day2 = date_value[2]; + return subtract_days(year2, month2, day2, days); +} +function shift_week(date_value, weeks) { + if (weeks === 0) { + return date_value; + } else { + let value3 = weeks; + if (value3 > 0) { + return add_days(date_value, value3 * 7); + } else { + let value4 = weeks; + return subtract_date_days(date_value, 0 - value4 * 7); + } + } +} +function current_week_dates(week_offset) { + let today = now(); + let _block; + let $ = day(today); + if ($ === 0) { + _block = 6; + } else { + let day2 = $; + _block = day2 - 1; + } + let days_from_monday = _block; + let current_week_start = subtract_days(year(today), month(today) + 1, date(today), days_from_monday); + let start5 = shift_week(current_week_start, week_offset); + let _pipe = toList([0, 1, 2, 3, 4, 5, 6]); + return map2(_pipe, (offset) => { + return [ + format_date_no_year(add_days(start5, offset)), + format_date(add_days(start5, offset)), + (() => { + let _pipe$1 = offset; + return num_to_day(_pipe$1); + })() + ]; + }); +} +function week_label(week_offset) { + let dates = current_week_dates(week_offset); + let _block; + let _pipe = first(dates); + _block = unwrap2(_pipe, ["", "", ""]); + let first2 = _block; + let _block$1; + let _pipe$1 = last(dates); + _block$1 = unwrap2(_pipe$1, ["", "", ""]); + let last2 = _block$1; + return first2[1] + " – " + last2[1]; +} +function calendar_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("calendar")])) + ])), + nav(List$Empty$const, toList([ + button(toList([on_click(Message$DashboardView$const)]), toList([text3("Board view")])), + 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$("panel")]), toList([ + h2(List$Empty$const, toList([text3("Find tasks")])), + input(toList([ + class$("task-search"), + placeholder("Search owner or assignees"), + on_input((var0) => { + return new TaskSearchChanged(var0); + }), + styles(toList([["width", "100%"], ["box-sizing", "border-box"]])) + ])), + select(toList([ + class$("subteam-filter"), + on_change((var0) => { + return new SubteamFilterChanged(var0); + }), + 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"] + ])) + ]), 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"), + styles(toList([["padding", "0"], ["overflow", "hidden"]])) + ]), toList([ + div(toList([ + styles(toList([ + ["display", "flex"], + ["justify-content", "space-between"], + ["align-items", "center"], + ["padding", "14px 16px"], + ["border-bottom", "1px solid #e2d9cc"] + ])) + ]), toList([ + button(toList([on_click(Message$PreviousWeek$const)]), toList([text3("← Previous week")])), + strong(List$Empty$const, toList([text3(week_label(model.calendar_week))])), + button(toList([on_click(Message$NextWeek$const)]), toList([text3("Next week →")])) + ])), + div(toList([ + class$("calendar-grid"), + styles(toList([ + ["display", "grid"], + ["grid-template-columns", "repeat(7, minmax(150px, 1fr))"], + ["overflow-x", "auto"] + ])) + ]), map2(current_week_dates(model.calendar_week), (x2) => { + let day_no_year = x2[0]; + let day2 = x2[1]; + let day_of_week = x2[2]; + return calendar_day(day_no_year, day_of_week, calendar_tasks(board.projects, model, day2)); + })) + ])) + ])); + } else { + return main(toList([class$("auth")]), toList([ + styles2(), + h1(List$Empty$const, toList([text3("Loading Iron Claw")])) + ])); + } +} function table_cell(children) { return td(toList([ styles(toList([ @@ -7911,18 +8324,6 @@ function task_row(row) { ])) ])); } -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([ @@ -7951,6 +8352,7 @@ function task_list_view(model) { ])), nav(List$Empty$const, toList([ button(toList([on_click(Message$DashboardView$const)]), toList([text3("Board view")])), + button(toList([on_click(Message$CalendarView$const)]), toList([text3("Calendar")])), button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) ])) ])), @@ -8150,6 +8552,8 @@ function view(model) { }); } else if ($ === "tasks") { return task_list_view(model); + } else if ($ === "calendar") { + return calendar_view(model); } else { return dashboard_view(model); } @@ -8161,7 +8565,7 @@ function remove_project(model, project_id) { let projects = filter(board.projects, (project) => { return project.id !== project_id; }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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, model.calendar_week); } else { return model; } @@ -8176,7 +8580,7 @@ function remove_task(model, task_id) { }); return new Project(project.id, project.name, project.description, project.archived, tasks); }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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, model.calendar_week); } else { return model; } @@ -8196,7 +8600,7 @@ function update_task_status(model, task_id, status) { }); return new Project(project.id, project.name, project.description, project.archived, tasks); }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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, model.calendar_week); } else { return model; } @@ -8253,12 +8657,12 @@ function update2(model, message2) { if ($ instanceof Ok) { let value3 = $[0]; return [ - 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), + 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, model.calendar_week), none() ]; } else { return [ - new Model("login", model.dashboard, new Some("Please sign in to continue."), 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("login", model.dashboard, new Some("Please sign in to continue."), 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, model.calendar_week), none() ]; } @@ -8275,7 +8679,7 @@ function update2(model, message2) { _block = new_project_json(_pipe); let body2 = _block; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, "", "", 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, model.dashboard, model.error, model.username, model.password, model.invite_password, "", "", 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, model.calendar_week), post("/api/projects", body2, expect_any_response((result) => { return new Saved((() => { let _pipe$1 = result; @@ -8298,7 +8702,7 @@ function update2(model, message2) { _block = new_task_json(_pipe); let body2 = _block; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search, model.calendar_week), post("/api/projects/" + to_string(project_id) + "/tasks", body2, expect_any_response((result) => { return new Saved((() => { let _pipe$1 = result; @@ -8331,14 +8735,14 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { 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), + 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, model.calendar_week), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("Invalid username or password."), 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, model.dashboard, new Some("Invalid username or password."), 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, model.calendar_week), none() ]; } @@ -8346,94 +8750,94 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search), + new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search, model.calendar_week), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("The request could not be saved."), 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, model.dashboard, new Some("The request could not be saved."), 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, model.calendar_week), none() ]; } } else if (message2 instanceof UsernameChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, value3, 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, model.dashboard, model.error, value3, 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, model.calendar_week), none() ]; } else if (message2 instanceof PasswordChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, value3, 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, model.dashboard, model.error, model.username, value3, 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, model.calendar_week), none() ]; } else if (message2 instanceof ProjectNameChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, value3, 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, model.dashboard, model.error, model.username, model.password, model.invite_password, value3, 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, model.calendar_week), none() ]; } else if (message2 instanceof ProjectDescriptionChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, value3, 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, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, value3, 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, model.calendar_week), none() ]; } else if (message2 instanceof TaskTitleChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, value3, 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, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, value3, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskDescriptionChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, value3, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, value3, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskAssigneeChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, value3, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, value3, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskAssigneesChanged) { let value3 = message2[0]; return [ - new Model(model.page, 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, value3, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), + new Model(model.page, 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, value3, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskDueDateChanged) { let value3 = message2[0]; return [ - new Model(model.page, 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, value3, model.subteam_filter, model.task_search), + new Model(model.page, 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, value3, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskSubteamChanged) { let subteam = message2[0]; let checked2 = message2[1]; return [ - new Model(model.page, 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, toggle_subteam(model.task_subteams, subteam, checked2), model.task_due_date, model.subteam_filter, model.task_search), + new Model(model.page, 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, toggle_subteam(model.task_subteams, subteam, checked2), model.task_due_date, model.subteam_filter, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof SubteamFilterChanged) { let value3 = message2[0]; return [ - new Model(model.page, 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, value3, model.task_search), + new Model(model.page, 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, value3, model.task_search, model.calendar_week), none() ]; } else if (message2 instanceof TaskSearchChanged) { let value3 = message2[0]; return [ - new Model(model.page, 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, value3), + new Model(model.page, 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, value3, model.calendar_week), none() ]; } else if (message2 instanceof InvitePasswordChanged) { let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, value3, 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, model.dashboard, model.error, model.username, model.password, value3, 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, model.calendar_week), none() ]; } else if (message2 instanceof DeleteTask) { @@ -8464,7 +8868,7 @@ function update2(model, message2) { ]; } else if (message2 instanceof LogOut) { return [ - new Model("login", 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), + new Model("login", 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, model.calendar_week), get3("/api/logout", expect_ok_response((_) => { return Message$Nothing$const; })) @@ -8499,12 +8903,27 @@ 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), + 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, model.calendar_week), + none() + ]; + } else if (message2 instanceof DashboardView) { + 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, model.calendar_week), + none() + ]; + } else if (message2 instanceof CalendarView) { + return [ + new Model("calendar", 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, model.calendar_week), + none() + ]; + } else if (message2 instanceof PreviousWeek) { + return [ + new Model(model.page, 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, model.calendar_week - 1), 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), + new Model(model.page, 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, model.calendar_week + 1), none() ]; } @@ -8523,11 +8942,13 @@ function init(_) { _block = "register"; } else if ($ === "/tasks") { _block = "tasks"; + } else if ($ === "/calendar") { + _block = "calendar"; } else { _block = "dashboard"; } let page = _block; - let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", "", List$Empty$const, "", "all", ""); + let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", "", List$Empty$const, "", "all", "", 0); if (page === "dashboard") { return [ model, @@ -8542,6 +8963,13 @@ function init(_) { return new DashboardLoaded(var0); })) ]; + } else if (page === "calendar") { + return [ + model, + get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { + return new DashboardLoaded(var0); + })) + ]; } else if (page === "login") { return [ model, @@ -8557,7 +8985,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", 23, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 527, end: 577, pattern_start: 538, pattern_end: 543 }); + 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 }); } return; }