month & shift navigation progress
This commit is contained in:
parent
b352416366
commit
4276399c20
20 changed files with 597 additions and 98 deletions
2
.iex.exs
2
.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
|
||||
|
|
|
@ -3,7 +3,7 @@ $primary: #662c91;
|
|||
$secondary: #ee6c4d;
|
||||
$success: #3f784c;
|
||||
$info: #3f84e5;
|
||||
$warning: #fcca46;
|
||||
$warning: #ffec51;
|
||||
$light: $gray-200;
|
||||
$dark: $gray-800;
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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%
|
||||
);
|
||||
}
|
||||
|
|
61
assets/js/_bs_collapse.js
Normal file
61
assets/js/_bs_collapse.js
Normal file
|
@ -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);
|
||||
// },
|
||||
// };
|
|
@ -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() {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) %>)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue