diff --git a/.iex.exs b/.iex.exs index 0cc62318..66306574 100644 --- a/.iex.exs +++ b/.iex.exs @@ -3,5 +3,7 @@ import Ecto.Query alias Shift73k.Repo alias Shift73k.Accounts alias Shift73k.Accounts.User +alias Shift73k.Shifts +alias Shift73k.Shifts.Shift alias Shift73k.Shifts.Templates alias Shift73k.Shifts.Templates.ShiftTemplate diff --git a/assets/css/_bs-colors.scss b/assets/css/_bs-colors.scss index a6648039..352e5727 100644 --- a/assets/css/_bs-colors.scss +++ b/assets/css/_bs-colors.scss @@ -3,7 +3,7 @@ $primary: #662c91; $secondary: #ee6c4d; $success: #3f784c; $info: #3f84e5; -$warning: #fcca46; +$warning: #ffec51; $light: $gray-200; $dark: $gray-800; diff --git a/assets/css/_bs-load.scss b/assets/css/_bs-load.scss index f1f0e784..b7c2aa2f 100644 --- a/assets/css/_bs-load.scss +++ b/assets/css/_bs-load.scss @@ -28,7 +28,7 @@ @import "../node_modules/bootstrap/scss/nav"; @import "../node_modules/bootstrap/scss/navbar"; @import "../node_modules/bootstrap/scss/card"; -// @import "../node_modules/bootstrap/scss/accordion"; +@import "../node_modules/bootstrap/scss/accordion"; // @import "../node_modules/bootstrap/scss/breadcrumb"; @import "../node_modules/bootstrap/scss/pagination"; // @import "../node_modules/bootstrap/scss/badge"; diff --git a/assets/css/app.scss b/assets/css/app.scss index a20a027c..722c55f2 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -63,12 +63,32 @@ } /* calendar table rounded */ -// table.table.table-rounded, -// table.table.table-rounded thead, -// table.table.table-rounded thead tr, -// table.table.table-rounded thead tr th:first-child { -// border-radius: 10px 0 0 10px; -// } +table.table.table-calendar thead tr th, +table.table.table-calendar tbody tr td { + width: 14%; +} +table.table.table-calendar tbody tr td { + font-size: $font-size-sm; + height: 3.5rem; + padding: 0.2rem 0.4rem; + @include media-breakpoint-up(md) { + height: 4.5rem; + padding: 0.2rem 0.5rem; + } + @include media-breakpoint-up(lg) { + font-size: $font-size-base; + height: 5.5rem; + padding: 0.3rem 0.5rem; + } + @include media-breakpoint-up(xl) { + height: 6.5rem; + padding: 0.35rem 0.5rem; + } + @include media-breakpoint-up(xxl) { + height: 7.5rem; + padding: 0.4rem 0.75rem; + } +} table.table.table-rounded { border-collapse: separate; @@ -117,3 +137,41 @@ table.table.table-rounded { } } } + +/* selected days background triangle color */ +.bg-triangle-primary { + background: linear-gradient( + to bottom right, + $primary 0%, + $primary 50%, + $secondary 50%, + $secondary 100% + ); +} +.bg-triangle-info { + background: linear-gradient( + to bottom right, + $info 0%, + $info 50%, + $secondary 50%, + $secondary 100% + ); +} +.bg-triangle-light { + background: linear-gradient( + to bottom right, + $light 0%, + $light 50%, + $secondary 50%, + $secondary 100% + ); +} +.bg-triangle-white { + background: linear-gradient( + to bottom right, + $white 0%, + $white 50%, + $secondary 50%, + $secondary 100% + ); +} diff --git a/assets/js/_bs_collapse.js b/assets/js/_bs_collapse.js new file mode 100644 index 00000000..c3c04074 --- /dev/null +++ b/assets/js/_bs_collapse.js @@ -0,0 +1,61 @@ +// Hook for custom liveview bootstrap collapse handling +import Collapse from "bootstrap/js/dist/collapse"; + +export const BsCollapse = { + mounted() { + // when the liveview mounts, create the BS collapse object + const collapse = new Collapse(this.el, { toggle: false }); + + this.handleEvent("toggle-template-details", ({ targetId }) => { + if (this.el.id == targetId) { + collapse.toggle(); + } + }); + + // this.el.addEventListener("show.bs.collapse", (event) => { + // this.pushEvent("collapse-show", { target_id: event.target.id }); + // }); + + this.el.addEventListener("shown.bs.collapse", (event) => { + this.pushEvent("collapse-shown", { target_id: event.target.id }); + }); + + // this.el.addEventListener("hide.bs.collapse", (event) => { + // this.pushEvent("collapse-hide", { target_id: event.target.id }); + // }); + + this.el.addEventListener("hidden.bs.collapse", (event) => { + this.pushEvent("collapse-hidden", { target_id: event.target.id }); + }); + }, +}; + +// export const BsModal = { +// mounted() { +// // when the liveview mounts, create the BS modal +// const modal = new Modal(this.el); +// // and trigger BS modal to show +// modal.show(); + +// // when the BS modal hides, send 'close' to the liveview +// this.el.addEventListener("hidden.bs.modal", (event) => { +// this.pushEventTo(`#${this.el.getAttribute("id")}`, "close", {}); +// modal.dispose(); +// }); + +// // liveview can send this event to tell BS modal to close +// // ex.: on successful form save, instead of immediate redirect +// // this event hides the BS modal, which triggers the above, +// // which sends 'close' to liveview and disposes the BS modal +// this.handleEvent("modal-please-hide", (payload) => { +// modal.hide(); +// }); +// }, + +// destroyed() { +// // when the liveview is destroyed, +// // modal-backdrop must be forcibly removed +// const backdrop = document.querySelector(".modal-backdrop"); +// if (backdrop) backdrop.parentElement.removeChild(backdrop); +// }, +// }; diff --git a/assets/js/_bs_modal.js b/assets/js/_bs_modal.js index 981b3bc9..ce2adb67 100644 --- a/assets/js/_bs_modal.js +++ b/assets/js/_bs_modal.js @@ -1,7 +1,6 @@ // Helping bootstrap modals work with liveview // preserving animations import Modal from "bootstrap/js/dist/modal"; -// import { Modal } from "bootstrap"; export const BsModal = { mounted() { diff --git a/assets/js/app.js b/assets/js/app.js index 7c7ad521..62e62df3 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -5,7 +5,7 @@ import "../css/app.scss"; // Import icons for sprite-loader // navbar brand icon -import "../node_modules/bootstrap-icons/icons/calendar3-week.svg"; // brand +import "../node_modules/bootstrap-icons/icons/calendar2-week.svg"; // brand // menus etc import "../node_modules/bootstrap-icons/icons/person-circle.svg"; // accounts menu import "../node_modules/bootstrap-icons/icons/person-plus.svg"; // new user / register @@ -34,9 +34,10 @@ import "../node_modules/bootstrap-icons/icons/arrow-repeat.svg"; // resend confi import "../node_modules/@mdi/svg/svg/head-question-outline.svg"; // forgot password import "../node_modules/bootstrap-icons/icons/people.svg"; // users management // calendar/event icons -import "../node_modules/bootstrap-icons/icons/calendar3.svg"; -import "../node_modules/bootstrap-icons/icons/calendar3-event.svg"; -import "../node_modules/bootstrap-icons/icons/calendar3-range.svg"; +import "../node_modules/bootstrap-icons/icons/calendar2.svg"; +import "../node_modules/bootstrap-icons/icons/calendar2-plus.svg"; +import "../node_modules/bootstrap-icons/icons/calendar2-event.svg"; +import "../node_modules/bootstrap-icons/icons/calendar2-range.svg"; import "../node_modules/bootstrap-icons/icons/clock-history.svg"; // shift template import "../node_modules/bootstrap-icons/icons/tag.svg"; import "../node_modules/bootstrap-icons/icons/hourglass.svg"; @@ -47,6 +48,11 @@ import "../node_modules/bootstrap-icons/icons/plus-circle-dotted.svg"; import "../node_modules/bootstrap-icons/icons/clipboard-plus.svg"; import "../node_modules/bootstrap-icons/icons/star.svg"; import "../node_modules/bootstrap-icons/icons/star-fill.svg"; +import "../node_modules/bootstrap-icons/icons/binoculars.svg"; +import "../node_modules/bootstrap-icons/icons/binoculars-fill.svg"; +import "../node_modules/bootstrap-icons/icons/eraser.svg"; +import "../node_modules/bootstrap-icons/icons/save.svg"; +import "../node_modules/bootstrap-icons/icons/asterisk.svg"; // webpack automatically bundles all modules in your // entry points. Those entry points can be configured @@ -72,11 +78,13 @@ import "./_form-validity"; // Bootstrap-liveview helpers import { AlertRemover } from "./_alert-remover"; import { BsModal } from "./_bs_modal"; +import { BsCollapse } from "./_bs_collapse"; // LiveSocket setup let Hooks = {}; Hooks.AlertRemover = AlertRemover; Hooks.BsModal = BsModal; +Hooks.BsCollapse = BsCollapse; let csrfToken = document .querySelector("meta[name='csrf-token']") .getAttribute("content"); diff --git a/lib/shift73k/shifts/templates.ex b/lib/shift73k/shifts/templates.ex index 0e0c251e..993f73a7 100644 --- a/lib/shift73k/shifts/templates.ex +++ b/lib/shift73k/shifts/templates.ex @@ -40,8 +40,10 @@ defmodule Shift73k.Shifts.Templates do ** (Ecto.NoResultsError) """ + def get_shift_template!(nil), do: nil def get_shift_template!(id), do: Repo.get!(ShiftTemplate, id) + def get_shift_template(nil), do: nil def get_shift_template(id), do: Repo.get(ShiftTemplate, id) @doc """ diff --git a/lib/shift73k/shifts/templates/shift_template.ex b/lib/shift73k/shifts/templates/shift_template.ex index 5896ce7d..55ad1a56 100644 --- a/lib/shift73k/shifts/templates/shift_template.ex +++ b/lib/shift73k/shifts/templates/shift_template.ex @@ -58,7 +58,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do [] end end) - |> validate_inclusion(:time_zone, Timex.timezones()) + |> validate_inclusion(:time_zone, Timex.timezones(), message: "must be a valid IANA tz database time zone") end defp time_start_from_attrs(%{"time_start" => time_start}), do: time_start diff --git a/lib/shift73k_web/live/live_helpers.ex b/lib/shift73k_web/live/live_helpers.ex index 6667f05a..a0b9ba77 100644 --- a/lib/shift73k_web/live/live_helpers.ex +++ b/lib/shift73k_web/live/live_helpers.ex @@ -5,6 +5,7 @@ defmodule Shift73kWeb.LiveHelpers do alias Shift73k.Accounts alias Shift73k.Accounts.User alias Shift73kWeb.UserAuth + alias Shift73k.Shifts.Templates.ShiftTemplate @doc """ Performs the {:noreply, socket} for a given socket. @@ -66,4 +67,17 @@ defmodule Shift73kWeb.LiveHelpers do put_flash(acc, String.to_existing_atom(k), v) end) end + + def format_shift_time(time), do: Timex.format!(time, "{h12}:{m}{am}") + + def format_shift_length(shift_template) do + shift_template + |> ShiftTemplate.shift_length() + |> Timex.Duration.from_minutes() + |> Timex.format_duration() + |> String.replace("PT", "") + |> String.replace("H", "h ") + |> String.replace("M", "m") + |> String.trim() + end end diff --git a/lib/shift73k_web/live/shift_assign_live/index.ex b/lib/shift73k_web/live/shift_assign_live/index.ex index 9ab3fec3..01e7326c 100644 --- a/lib/shift73k_web/live/shift_assign_live/index.ex +++ b/lib/shift73k_web/live/shift_assign_live/index.ex @@ -3,17 +3,98 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do use Timex alias Shift73k.EctoEnums.WeekdayEnum + alias Shift73k.Shifts.Templates + alias Shift73k.Shifts.Templates.ShiftTemplate + + @custom_shift %ShiftTemplate{subject: "Custom shift", id: "custom-shift"} + @custom_shift_opt {@custom_shift.subject, @custom_shift.id} @impl true def mount(_params, session, socket) do socket |> assign_defaults(session) - |> assign_day_names() - |> assign_init_dates(Timex.today()) - |> assign_week_rows() + |> assign(:custom_shift, @custom_shift) + |> assign(:show_template_btn_active, :false) + |> assign(:show_template_details, :false) + |> assign(:selected_days, []) |> live_okreply() end + @impl true + def handle_params(_params, _url, socket) do + socket + |> init_shift_templates() + |> init_shift_template() + |> show_details_if_custom() + |> assign_shift_length() + |> assign_shift_template_changeset() + |> init_today(Timex.today()) + |> init_calendar() + |> init_known_shifts() + |> live_noreply() + end + + defp get_shift_template("custom-shift"), do: @custom_shift + defp get_shift_template(template_id), do: Templates.get_shift_template(template_id) + + defp assign_shift_length(%{assigns: %{shift_template: shift}} = socket) do + assign(socket, :shift_length, format_shift_length(shift)) + end + + defp init_known_shifts(%{assigns: %{current_user: user, week_rows: weeks}} = socket) do + days = weeks |> List.flatten() + first = weeks |> List.flatten() |> List.first() + socket + end + + defp init_calendar(%{assigns: %{current_user: user}} = socket) do + days = day_names(user.week_start_at) + {first, last, rows} = week_rows(socket.assigns.cursor_date, user.week_start_at) + assign(socket, [day_names: days, week_rows: rows, day_first: first, day_last: last]) + end + + defp init_today(socket, today) do + assign(socket, [current_date: today, cursor_date: today]) + end + + defp assign_shift_template_changeset(%{assigns: %{shift_template: shift}} = socket) do + cs = Templates.change_shift_template(shift) + assign(socket, :shift_template_changeset, cs) + end + + defp init_shift_template(socket) do + first_list_id = socket.assigns.shift_templates |> hd() |> elem(1) + fave_id = socket.assigns.current_user.fave_shift_template_id + assign_shift_template(socket, (fave_id || first_list_id)) + end + + defp assign_shift_template(socket, template_id) do + assign(socket, :shift_template, get_shift_template(template_id)) + end + + defp init_shift_templates(%{assigns: %{current_user: user}} = socket) do + shift_templates = + Templates.list_shift_templates_by_user_id(user.id) + |> Enum.map(fn t -> shift_template_option(t, user.fave_shift_template_id) end) + |> Enum.concat([@custom_shift_opt]) + assign(socket, :shift_templates, shift_templates) + end + + defp shift_template_option(template, fave_id) do + label = + template.subject <> " (" <> + format_shift_time(template.time_start) <> "—" <> + format_shift_time(template.time_end) <> ")" + + label = + case fave_id == template.id do + true -> label <> " ★" + false -> label + end + + {label, template.id} + end + defp rotate_week(week_start_at) do {a, b} = Enum.split_while(WeekdayEnum.__enum_map__(), fn {k, _v} -> k != week_start_at end) b ++ a @@ -37,46 +118,160 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do |> Timex.end_of_month() |> Timex.end_of_week(week_start_at) - Interval.new(from: first, until: last, right_open: false) - |> Enum.map(& &1) - |> Enum.chunk_every(7) + week_rows = + Interval.new(from: first, until: last, right_open: false) + |> Enum.map(& NaiveDateTime.to_date(&1)) + |> Enum.chunk_every(7) + + {first, last, week_rows} end - defp assign_day_names(socket) do - day_names = day_names(socket.assigns.current_user.week_start_at) - assign(socket, :day_names, day_names) + def day_color(day, current_date, cursor_date, selected_days) do + cond do + Enum.member?(selected_days, Date.to_string(day)) -> + cond do + Timex.compare(day, current_date, :days) == 0 -> "bg-triangle-info text-white" + day.month != cursor_date.month -> "bg-triangle-light text-gray" + true -> "bg-triangle-white" + end + + Timex.compare(day, current_date, :days) == 0 -> "bg-info text-white" + + day.month != cursor_date.month -> "bg-light text-gray" + + true -> "" + end end - defp assign_init_dates(socket, today) do - assign(socket, [current_date: today, cursor_date: today]) + defp prep_template_params(params, current_user) do + params + |> Map.put("time_start", Time.from_iso8601!("T#{params["time_start"]}:00")) + |> Map.put("time_end", Time.from_iso8601!("T#{params["time_end"]}:00")) + |> Map.put("user_id", current_user.id) end - defp assign_week_rows(socket) do - cursor_date = socket.assigns.cursor_date - week_start_at = socket.assigns.current_user.week_start_at - - assign(socket, :week_rows, week_rows(cursor_date, week_start_at)) + defp show_details_if_custom(socket) do + if (socket.assigns.shift_template.id != @custom_shift.id) || socket.assigns.show_template_details do + socket + else + socket + |> assign(:show_template_btn_active, :true) + |> push_event("toggle-template-details", %{targetId: "#templateDetailsCol"}) + end end - def handle_event("prev-month", _, socket) do - cursor_date = Timex.shift(socket.assigns.cursor_date, months: -1) + defp set_month(socket, new_cursor_date) do + {first, last, rows} = week_rows(new_cursor_date, socket.assigns.current_user.week_start_at) assigns = [ - cursor_date: cursor_date, - week_rows: week_rows(cursor_date, socket.assigns.current_user.week_start_at) + cursor_date: new_cursor_date, + week_rows: rows, + day_first: first, + day_last: last ] - {:noreply, assign(socket, assigns)} + assign(socket, assigns) end - def handle_event("next-month", _, socket) do - cursor_date = Timex.shift(socket.assigns.cursor_date, months: 1) + @impl true + def handle_event("validate-shift-template", %{"shift_template" => params}, socket) do + params = prep_template_params(params, socket.assigns.current_user) + shift_template = socket.assigns.shift_template - assigns = [ - cursor_date: cursor_date, - week_rows: week_rows(cursor_date, socket.assigns.current_user.week_start_at) - ] + cs = + shift_template + |> Templates.change_shift_template(params) + |> Map.put(:action, :validate) - {:noreply, assign(socket, assigns)} + socket + |> assign(:shift_template_changeset, cs) + |> assign(:shift_template, Map.merge(shift_template, cs.changes)) + |> assign_shift_length() + |> live_noreply() + end + + @impl true + def handle_event("change-selected-template", %{"template_select" => %{"template" => template_id}}, socket) do + socket + |> assign_shift_template(template_id) + |> show_details_if_custom() + |> assign_shift_length() + |> assign_shift_template_changeset() + |> live_noreply() + end + + @impl true + def handle_event("month-nav", %{"month" => direction}, socket) do + new_cursor = + cond do + direction == "now" -> Timex.today() + true -> + months = m = direction == "prev" && -1 || 1 + Timex.shift(socket.assigns.cursor_date, months: months) + end + + {:noreply, set_month(socket, new_cursor)} + end + + # @impl true + # def handle_event("prev-month", _, socket) do + # cursor_date = Timex.shift(socket.assigns.cursor_date, months: -1) + # {first, last, rows} = week_rows(cursor_date, socket.assigns.current_user.week_start_at) + + # assigns = [ + # cursor_date: cursor_date, + # week_rows: week_rows(cursor_date, socket.assigns.current_user.week_start_at) + # ] + + # {:noreply, assign(socket, assigns)} + # end + + # @impl true + # def handle_event("next-month", _, socket) do + # cursor_date = Timex.shift(socket.assigns.cursor_date, months: 1) + # {first, last, rows} = week_rows(cursor_date, socket.assigns.current_user.week_start_at) + + # assigns = [ + # cursor_date: cursor_date, + # week_rows: rows, + # day_first: first, + # day_last: last + # ] + + # {:noreply, assign(socket, assigns)} + # end + + @impl true + def handle_event("toggle-template-details", %{"target_id" => target_id}, socket) do + socket + |> assign(:show_template_btn_active, !socket.assigns.show_template_btn_active) + |> push_event("toggle-template-details", %{targetId: target_id}) + |> live_noreply() + end + + @impl true + def handle_event("collapse-shown", %{"target_id" => _target_id}, socket) do + {:noreply, assign(socket, :show_template_details, :true)} + end + + @impl true + def handle_event("collapse-hidden", %{"target_id" => _target_id}, socket) do + {:noreply, assign(socket, :show_template_details, :false)} + end + + @impl true + def handle_event("select-day", %{"day" => day}, socket) do + selected_days = + case day_index = Enum.find_index(socket.assigns.selected_days, fn d -> d == day end) do + nil -> [day | socket.assigns.selected_days] + _ -> List.delete_at(socket.assigns.selected_days, day_index) + end + + {:noreply, assign(socket, :selected_days, selected_days)} + end + + @impl true + def handle_event("clear-days", _params, socket) do + {:noreply, assign(socket, :selected_days, [])} end end diff --git a/lib/shift73k_web/live/shift_assign_live/index.html.leex b/lib/shift73k_web/live/shift_assign_live/index.html.leex index c47db54a..6fe3c5f8 100644 --- a/lib/shift73k_web/live/shift_assign_live/index.html.leex +++ b/lib/shift73k_web/live/shift_assign_live/index.html.leex @@ -1,31 +1,192 @@ <h2 class="mb-3 mb-sm-0"> - <%= icon_div @socket, "bi-calendar3", [class: "icon baseline"] %> + <%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %> Assign Shift To Dates </h2> +<div class="row justify-content-center mt-4"> + <div class="col-12 col-lg-9 col-xl-8 col-xxl-7 d-flex justify-content-start align-items-end"> + + <%= form_for :template_select, "#", [phx_change: "change-selected-template"], fn sts -> %> + <%= label sts, :template, "Select shift template to assign to dates", class: "form-label" %> + <%= select sts, :template, @shift_templates, + value: @shift_template && @shift_template.id || (@shift_template_options |> hd() |> elem(1)), + class: "form-select" + %> + <% end %> + + <button type="button" class="ms-2 btn btn-primary text-nowrap <%= if @show_template_btn_active, do: "active" %>" id="#templateDetailsBtn" phx-click="toggle-template-details" phx-value-target_id="#templateDetailsCol"> + <%= icon_div @socket, (@show_template_btn_active && "bi-binoculars-fill" || "bi-binoculars"), [class: "icon baseline", style: "margin-right:0.125rem;"] %> + Details + </button> + + </div> + + <div class="col-12 col-lg-9 col-xl-8 col-xxl-7 <%= @show_template_details && "collapse show" || "collapse" %>" id="#templateDetailsCol" phx-hook="BsCollapse"> + <div class="card mt-4"> + <div class="card-body"> + + <%= form_for @shift_template_changeset, "#", [phx_change: "validate-shift-template"], fn stf -> %> + + <div class="row"> + + <div class="col-12 col-md-6"> + <%= label stf, :subject, "Subject/Title", class: "form-label" %> + <div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :subject) %>"> + <%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %> + <%= text_input stf, :subject, + value: input_value(stf, :subject), + class: input_class(stf, :subject, "form-control"), + phx_debounce: 250, + disabled: @shift_template.id != @custom_shift.id, + aria_describedby: error_ids(stf, :subject) + %> + <%= error_tag stf, :subject %> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="row gx-2 gx-sm-3"> + + <div class="col-6" phx-feedback-for="<%= input_id(stf, :time_start) %>"> + <%= label stf, :time_start, "Start", class: "form-label" %> + <%= time_input stf, :time_start, + precision: :minute, + value: input_value(stf, :time_start), + class: input_class(stf, :time_start, "form-control"), + disabled: @shift_template.id != @custom_shift.id, + aria_describedby: error_ids(stf, :time_start) + %> + </div> + + <div class="col-6" phx-feedback-for="<%= input_id(stf, :time_end) %>"> + <%= label stf, :time_end, "End", class: "form-label" %> + <%= time_input stf, :time_end, + precision: :minute, + value: input_value(stf, :time_end), + class: input_class(stf, :time_end, "form-control"), + disabled: @shift_template.id != @custom_shift.id, + aria_describedby: error_ids(stf, :time_end) + %> + </div> + + </div> + + <div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div> + + <div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_start) %>"> + <%= error_tag stf, :time_start %> + </div> + <div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_end) %>"> + <%= error_tag stf, :time_end %> + </div> + </div> + + <div class="col-12 col-md-6"> + <%= label stf, :location, class: "form-label" %> + <div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :location) %>"> + <%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %> + <%= text_input stf, :location, + value: input_value(stf, :location), + class: input_class(stf, :location, "form-control"), + phx_debounce: 250, + disabled: @shift_template.id != @custom_shift.id, + aria_describedby: error_ids(stf, :location) + %> + <%= error_tag stf, :location %> + </div> + </div> + + <div class="col-12 col-md-6"> + <%= label stf, :time_zone, class: "form-label" %> + <div class="inner-addon left-addon mb-3 mb-md-0" phx-feedback-for="<%= input_id(stf, :time_zone) %>"> + <%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %> + <%= text_input stf, :time_zone, + value: input_value(stf, :time_zone), + class: input_class(stf, :time_zone, "form-control"), + disabled: @shift_template.id != @custom_shift.id, + phx_debounce: 250, + list: "tz_list" + %> + <datalist id="tz_list"> + <%= for tz_name <- Timex.timezones() do %> + <option value="<%= tz_name %>"></option> + <% end %> + end + </datalist> + <%= if @shift_template.id == @custom_shift.id do %> + <div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div> + <% end %> + <%= error_tag stf, :time_zone %> + </div> + </div> + + <div class="col-12"> + <%= label stf, :description, class: "form-label" %> + <div phx-feedback-for="<%= input_id(stf, :description) %>"> + + <%= textarea stf, :description, + value: input_value(stf, :description), + class: input_class(stf, :description, "form-control"), + disabled: @shift_template.id != @custom_shift.id, + phx_debounce: 250, + aria_describedby: error_ids(stf, :description) + %> + <%= error_tag stf, :description %> + </div> + </div> + + </div> + + <button type="submit" class="d-hidden"></button> + + <% end %> + + </div> + </div> + + </div> +</div> + + + + + + + + + + + + + + <%# month navigation %> -<div class="d-flex justify-content-between align-items-baseline mt-4"> - <h3 class="ms-4 text-muted mb-0"> - <%= Timex.format!(@cursor_date, "%B %Y", :strftime) %> +<div class="d-flex justify-content-between align-items-end mt-4"> + <h3 class="text-muted mb-0"> + <%= Timex.format!(@cursor_date, "{Mfull} {YYYY}") %> </h3> - <div class="me-4"> - <a href="#" phx-click="prev-month" class="btn btn-primary"> - <%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %> + <div> + <button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Timex.today(), :month), do: "disabled" %>> + <%= icon_div @socket, "bi-asterisk", [class: "icon baseline", style: "margin-right:0.125rem;"] %> + Today + </button> + <button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary"> + <%= icon_div @socket, "bi-chevron-left", [class: "icon baseline", style: "margin-right:0.125rem;"] %> Prev - </a> - <a href="#" phx-click="next-month" class="btn btn-primary"> + </button> + <button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary"> Next - <%= icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %> - </a> + <%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %> + </button> </div> </div> <%# calendar month table display %> -<table class="table table-rounded shadow mt-3"> +<table class="table table-rounded table-calendar shadow mt-3"> <thead> <tr> <%= for {day_name, _i} <- Enum.with_index(@day_names) do %> - <th width="14%"> + <th> <%= day_name %> </th> <% end %> @@ -35,23 +196,29 @@ <%= for week <- @week_rows do %> <tr> <%= for day <- week do %> - <%= cond do %> - - <% Timex.compare(day, @current_date, :days) == 0 -> %> - <td width="14%" style="height: 6rem;" class="bg-info text-white"> - - <% day.month != @cursor_date.month -> %> - <td width="14%" style="height: 6rem;" class="bg-light text-gray"> - - <% true -> %> - <td width="14%" style="height: 6rem;"> - - <% end %> - - <%= Timex.format!(day, "%d", :strftime) %> + <%# day |> NaiveDateTime.to_date() |> IO.inspect() %> + <td class="<%= day_color(day, @current_date, @cursor_date, @selected_days) %>" phx-click="select-day" phx-value-day="<%= day %>"> + <%= Timex.format!(day, "{0D}") %><%= if day.month != @cursor_date.month, do: "-#{Timex.format!(day, "{Mshort}")}" %> </td> <% end %> </tr> <% end %> </tbody> </table> + + +<div class="row justify-content-end mt-4"> + <div class="col-auto"> + + <button class="btn btn-warning" phx-click="clear-days"> + <%= icon_div @socket, "bi-eraser", [class: "icon baseline", style: "margin-right:0.125rem;"] %> + Clear + </button> + + <button class="btn btn-primary" phx-click="save-days"> + <%= icon_div @socket, "bi-save", [class: "icon baseline", style: "margin-right:0.125rem;"] %> + Save assigned shifts + </button> + + </div> +</div> diff --git a/lib/shift73k_web/live/shift_template_live/delete_component.html.leex b/lib/shift73k_web/live/shift_template_live/delete_component.html.leex index d43ce9d1..a14f6a74 100644 --- a/lib/shift73k_web/live/shift_template_live/delete_component.html.leex +++ b/lib/shift73k_web/live/shift_template_live/delete_component.html.leex @@ -1,9 +1,9 @@ <div class="modal-body"> Are you sure you want to delete "<%= @delete_shift_template.subject %> - (<%= @delete_shift_template.time_start |> Timex.format!("{h12}:{m}{am}") %> + (<%= format_shift_time(@delete_shift_template.time_start) %> — - <%= @delete_shift_template.time_end |> Timex.format!("{h12}:{m}{am}") %>)"? + <%= format_shift_time(@delete_shift_template.time_end) %>)"? </div> <div class="modal-footer"> diff --git a/lib/shift73k_web/live/shift_template_live/form_component.ex b/lib/shift73k_web/live/shift_template_live/form_component.ex index 33761b37..f1790457 100644 --- a/lib/shift73k_web/live/shift_template_live/form_component.ex +++ b/lib/shift73k_web/live/shift_template_live/form_component.ex @@ -1,8 +1,6 @@ defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do use Shift73kWeb, :live_component - import Shift73kWeb.ShiftTemplateLive.Index, only: [format_shift_length: 1] - alias Shift73k.Shifts.Templates alias Shift73k.Shifts.Templates.ShiftTemplate diff --git a/lib/shift73k_web/live/shift_template_live/form_component.html.leex b/lib/shift73k_web/live/shift_template_live/form_component.html.leex index 73f0d7cb..8ea39d8b 100644 --- a/lib/shift73k_web/live/shift_template_live/form_component.html.leex +++ b/lib/shift73k_web/live/shift_template_live/form_component.html.leex @@ -13,6 +13,7 @@ value: input_value(f, :subject), class: input_class(f, :subject, "form-control"), autofocus: true, + phx_debounce: 250, aria_describedby: error_ids(f, :subject) %> <%= error_tag f, :subject %> @@ -59,6 +60,7 @@ <%= text_input f, :location, value: input_value(f, :location), class: input_class(f, :location, "form-control"), + phx_debounce: 250, aria_describedby: error_ids(f, :location) %> <%= error_tag f, :location %> @@ -71,6 +73,7 @@ <%= textarea f, :description, value: input_value(f, :description), class: input_class(f, :description, "form-control"), + phx_debounce: 250, aria_describedby: error_ids(f, :description) %> <%= error_tag f, :description %> @@ -82,7 +85,8 @@ <%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %> <%= text_input f, :time_zone, value: input_value(f, :time_zone), - class: "form-control", + class: input_class(f, :time_zone, "form-control"), + phx_debounce: 250, list: "tz_list" %> <datalist id="tz_list"> @@ -91,7 +95,7 @@ <% end %> end </datalist> - <div class="valid-feedback d-block text-primary">Type to search & select from list of known time zones (<%= link "formatted per IANA", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %>)</div> + <div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div> <%= error_tag f, :time_zone %> </div> diff --git a/lib/shift73k_web/live/shift_template_live/index.ex b/lib/shift73k_web/live/shift_template_live/index.ex index 22ece55f..af877f65 100644 --- a/lib/shift73k_web/live/shift_template_live/index.ex +++ b/lib/shift73k_web/live/shift_template_live/index.ex @@ -107,14 +107,4 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do socket |> put_flash(flash_type, msg) |> live_noreply() end - def format_shift_length(shift_template) do - shift_template - |> ShiftTemplate.shift_length() - |> Timex.Duration.from_minutes() - |> Timex.format_duration() - |> String.replace("PT", "") - |> String.replace("H", "h ") - |> String.replace("M", "m") - |> String.trim() - end end diff --git a/lib/shift73k_web/live/shift_template_live/index.html.leex b/lib/shift73k_web/live/shift_template_live/index.html.leex index 56b03aaf..75e40f00 100644 --- a/lib/shift73k_web/live/shift_template_live/index.html.leex +++ b/lib/shift73k_web/live/shift_template_live/index.html.leex @@ -25,7 +25,7 @@ My Shift Templates </h2> <%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %> - <%= icon_div @socket, "bi-plus-circle-dotted", [class: "icon baseline me-1"] %> + <%= icon_div @socket, "bi-plus-circle-dotted", [class: "icon baseline", style: "margin-right:0.125rem;"] %> New Shift Template <% end %> </div> @@ -38,13 +38,14 @@ <div class="col-12 col-lg-6"> <div class="card mt-4"> - <h5 class="card-header d-flex justify-content-between align-items-start"> - <div class="visually-hidden">Subject:</div> - <%= template.subject %> + <h5 class="card-header d-flex justify-content-between align-items-center"> + <span class="visually-hidden">Subject:</span> + <%= icon_div @socket, "bi-tag", [class: "icon baseline me-1"] %> + <div class="w-100"><%= template.subject %></div> <%= if template.id == @current_user.fave_shift_template_id do %> - <%= icon_div @socket, "bi-star-fill", [class: "icon baseline text-primary align-self-start ms-2", phx_click: "unset-user-fave-shift-template"] %> + <%= icon_div @socket, "bi-star-fill", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Unset as favorite", phx_click: "unset-user-fave-shift-template", class: "cursor-pointer"] %> <% else %> - <%= icon_div @socket, "bi-star", [class: "icon baseline text-primary align-self-start ms-2", phx_click: "set-user-fave-shift-template", phx_value_id: template.id] %> + <%= icon_div @socket, "bi-star", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Set as favorite", phx_click: "set-user-fave-shift-template", phx_value_id: template.id, class: "cursor-pointer"] %> <% end %> </h5> <div class="card-body"> @@ -53,13 +54,13 @@ <tbody> <tr> <th scope="row" class="text-end"> - <%= icon_div @socket, "bi-hourglass", [class: "icon text-muted"] %> + <%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %> <span class="visually-hidden">Hours:</span> </th> <td> - <%= template.time_start |> Timex.format!("{h12}:{m}{am}") %> + <%= format_shift_time(template.time_start) %> — - <%= template.time_end |> Timex.format!("{h12}:{m}{am}") %> + <%= format_shift_time(template.time_end) %> <span class="text-muted"> <span class="visually-hidden">Shift length:</span> (<%= format_shift_length(template) %>) diff --git a/lib/shift73k_web/live/user/settings/week_start.html.leex b/lib/shift73k_web/live/user/settings/week_start.html.leex index fe95ba87..1d5221f8 100644 --- a/lib/shift73k_web/live/user/settings/week_start.html.leex +++ b/lib/shift73k_web/live/user/settings/week_start.html.leex @@ -6,7 +6,7 @@ <%= label cvf, :week_start_at, class: "form-label" %> <div class="inner-addon left-addon mb-3"> - <%= icon_div @socket, "bi-calendar3-range", [class: "icon is-left text-muted fs-5"] %> + <%= icon_div @socket, "bi-calendar2-range", [class: "icon is-left text-muted fs-5"] %> <%= select cvf, :week_start_at, week_start_options(), value: @current_user.week_start_at, class: "form-select" diff --git a/lib/shift73k_web/live/user_management/index.html.leex b/lib/shift73k_web/live/user_management/index.html.leex index cb38f2d6..d4f539ac 100644 --- a/lib/shift73k_web/live/user_management/index.html.leex +++ b/lib/shift73k_web/live/user_management/index.html.leex @@ -1,5 +1,5 @@ <%= if @live_action in [:new, :edit] do %> - <%= live_modal @socket, Bones73kWeb.UserManagement.FormComponent, + <%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent, id: @user.id || :new, title: @page_title, action: @live_action, @@ -8,7 +8,7 @@ <% end %> <%= if @delete_user do %> - <%= live_modal @socket, Bones73kWeb.UserManagement.DeleteComponent, + <%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent, id: @delete_user.id, title: "Delete User", delete_user: @delete_user, diff --git a/lib/shift73k_web/templates/layout/_navbar.html.eex b/lib/shift73k_web/templates/layout/_navbar.html.eex index 611fa08a..e4dff3bf 100644 --- a/lib/shift73k_web/templates/layout/_navbar.html.eex +++ b/lib/shift73k_web/templates/layout/_navbar.html.eex @@ -3,7 +3,7 @@ <h1 class="fs-4 my-0 py-0 lh-base"> <%= link to: Routes.page_path(@conn, :index), class: "navbar-brand fs-4" do %> - <%= icon_div @conn, "bi-calendar3-week", [class: "icon baseline me-1"] %> + <%= icon_div @conn, "bi-calendar2-week", [class: "icon baseline me-1"] %> <span class="fw-light">Shift73k</span> <% end %> </h1>