much progress on shift assigning & app navigation

This commit is contained in:
Adam Piontek 2021-03-19 16:38:52 -04:00
parent 4541070f75
commit 8957f2d1dd
33 changed files with 363 additions and 330 deletions

View file

@ -22,13 +22,26 @@ defmodule Shift73k.Shifts do
end end
def list_shifts_by_user_between_dates(user_id, start_date, end_date) do def list_shifts_by_user_between_dates(user_id, start_date, end_date) do
q = from( from(s in Shift)
s in Shift, |> select([s], %{
select: %{date: s.date, subject: s.subject, time_start: s.time_start, time_end: s.time_end}, date: s.date,
where: s.user_id == ^user_id and s.date >= ^start_date and s.date < ^end_date, subject: s.subject,
order_by: [s.date, s.time_start] time_start: s.time_start,
) time_end: s.time_end
Repo.all(q) })
|> where([s], s.user_id == ^user_id and s.date >= ^start_date and s.date <= ^end_date)
|> order_by([s], [s.date, s.time_start])
|> Repo.all()
end
defp query_shifts_by_user_on_list_of_dates(user_id, date_list) do
from(s in Shift)
|> where([s], s.user_id == ^user_id and s.date in ^date_list)
end
def list_shifts_by_user_on_list_of_dates(user_id, date_list) do
query_shifts_by_user_on_list_of_dates(user_id, date_list)
|> Repo.all()
end end
@doc """ @doc """
@ -99,6 +112,11 @@ defmodule Shift73k.Shifts do
Repo.delete(shift) Repo.delete(shift)
end end
def delete_shifts_by_user_on_list_of_dates(user_id, date_list) do
query_shifts_by_user_on_list_of_dates(user_id, date_list)
|> Repo.delete_all()
end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking shift changes. Returns an `%Ecto.Changeset{}` for tracking shift changes.

View file

@ -22,7 +22,11 @@ defmodule Shift73k.Shifts.Templates do
end end
def list_shift_templates_by_user_id(user_id) do def list_shift_templates_by_user_id(user_id) do
q = from s in ShiftTemplate, where: s.user_id == ^user_id, order_by: [s.subject, s.time_start] q =
from s in ShiftTemplate,
where: s.user_id == ^user_id,
order_by: [fragment("lower(?)", s.subject), s.time_start]
Repo.all(q) Repo.all(q)
end end

View file

@ -19,6 +19,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
field :time_end, :time, default: ~T[17:00:00] field :time_end, :time, default: ~T[17:00:00]
belongs_to(:user, Shift73k.Accounts.User) belongs_to(:user, Shift73k.Accounts.User)
has_one(:is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id)
timestamps() timestamps()
end end
@ -58,7 +59,9 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
[] []
end end
end) end)
|> validate_inclusion(:time_zone, Timex.timezones(), message: "must be a valid IANA tz database time zone") |> validate_inclusion(:time_zone, Timex.timezones(),
message: "must be a valid IANA tz database time zone"
)
end end
defp time_start_from_attrs(%{"time_start" => time_start}), do: time_start defp time_start_from_attrs(%{"time_start" => time_start}), do: time_start

View file

@ -1,16 +0,0 @@
defmodule Shift73kWeb.OtherController do
use Shift73kWeb, :controller
def index(conn, _params) do
conn
|> put_flash(:success, "Log in was a success. Good for you.")
|> put_flash(:error, "Lorem ipsum dolor sit amet consectetur adipisicing elit.")
|> put_flash(
:info,
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus dolore sunt quia aperiam sint id reprehenderit? Dolore incidunt alias inventore accusantium nulla optio, ducimus eius aliquam hic, pariatur voluptate distinctio."
)
|> put_flash(:warning, "Oh no, there's nothing to worry about!")
|> put_flash(:primary, "Something in the brand color.")
|> render("index.html")
end
end

View file

@ -16,10 +16,10 @@ defmodule Shift73kWeb.ModalComponent do
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5> <h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" phx-click="hide" phx-target="<%= @myself %>" aria-label="Close"></button>
</div> </div>
<%= live_component @socket, @component, @opts %> <%= live_component @socket, @component, Keyword.put(@opts, :modal_id, @id) %>
</div> </div>
</div> </div>
@ -27,6 +27,11 @@ defmodule Shift73kWeb.ModalComponent do
""" """
end end
@impl true
def update(assigns, socket) do
socket |> assign(assigns) |> live_okreply()
end
@impl true @impl true
def handle_event("close", _, socket) do def handle_event("close", _, socket) do
send(self(), {:close_modal, true}) send(self(), {:close_modal, true})
@ -35,6 +40,6 @@ defmodule Shift73kWeb.ModalComponent do
@impl true @impl true
def handle_event("hide", _, socket) do def handle_event("hide", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})} socket |> push_event("modal-please-hide", %{}) |> live_noreply()
end end
end end

View file

@ -1,40 +0,0 @@
defmodule Shift73kWeb.PageLive do
use Shift73kWeb, :live_view
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(socket, session)
{:ok, assign(socket, query: "", results: %{})}
end
@impl true
def handle_event("suggest", %{"q" => query}, socket) do
{:noreply, assign(socket, results: search(query), query: query)}
end
@impl true
def handle_event("search", %{"q" => query}, socket) do
case search(query) do
%{^query => vsn} ->
{:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
_ ->
{:noreply,
socket
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|> assign(results: %{}, query: query)}
end
end
defp search(query) do
if not Shift73kWeb.Endpoint.config(:code_reloader) do
raise "action disabled when not in development"
end
for {app, desc, vsn} <- Application.started_applications(),
app = to_string(app),
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
into: %{},
do: {app, vsn}
end
end

View file

@ -1,49 +0,0 @@
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>Peace of mind from prototype to production</p>
<form phx-change="suggest" phx-submit="search">
<input type="text" name="q" value="<%= @query %>" placeholder="Live dependency search" list="results" autocomplete="off"/>
<datalist id="results">
<%= for {app, _vsn} <- @results do %>
<option value="<%= app %>"><%= app %></option>
<% end %>
</datalist>
<button type="submit" phx-disable-with="Searching...">Go to Hexdocs</button>
</form>
</section>
<section class="row align-items-start">
<article class="col">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
</li>
</ul>
</article>
<article class="col">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
<li>
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
</li>
</ul>
</article>
</section>

View file

@ -0,0 +1,42 @@
defmodule Shift73kWeb.ShiftAssignLive.DeleteComponent do
use Shift73kWeb, :live_component
alias Shift73k.Shifts
@impl true
def update(assigns, socket) do
socket
|> assign(assigns)
|> assign_dates()
|> live_okreply()
end
defp assign_dates(%{assigns: %{delete_days_shifts: daylist}} = socket) do
date_list = Enum.map(daylist, &Date.from_iso8601!/1)
year_map = Enum.group_by(date_list, fn d -> d.year end)
assign(socket, date_list: date_list, date_map: build_date_map(year_map))
end
def build_date_map(year_map) do
year_map
|> Map.keys()
|> Enum.reduce(year_map, fn y, acc ->
Map.put(acc, y, Enum.group_by(acc[y], fn d -> d.month end))
end)
end
@impl true
def handle_event("confirm-delete-days-shifts", _params, socket) do
user = socket.assigns.current_user
date_list = socket.assigns.date_list
{n, _} = Shifts.delete_shifts_by_user_on_list_of_dates(user.id, date_list)
s = (n > 1 && "s") || ""
flash = {:info, "Successfully deleted #{n} assigned shift#{s}"}
send(self(), {:put_flash_message, flash})
send(self(), {:clear_selected_days, true})
socket
|> push_event("modal-please-hide", %{})
|> live_noreply()
end
end

View file

@ -0,0 +1,31 @@
<div class="modal-body">
<p>Are you sure you want to delete all assigned shifts from the selected days?</p>
<%= for {y, data} <- @date_map do %>
<dt><%= y %></dt>
<% months = Map.keys(data) %>
<dd>
<%= for {m, i} <- Enum.with_index(months, 1) do %>
<%= data |> Map.get(m) |> hd() |> Timex.format!("{Mshort}") %>:
<% days = Map.get(data, m) %>
<%= for {d, i} <- Enum.with_index(days, 1) do %>
<%= d.day %><%= if i < length(days) do %>,<% end %>
<% end %>
<%= if i < length(months) do %><br /><% end %>
<% end %>
</dd>
<% end %>
</div>
<div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
<%= link "Confirm Delete", to: "#",
class: "btn btn-danger",
phx_click: "confirm-delete-days-shifts",
phx_target: @myself
%>
</div>

View file

@ -18,9 +18,10 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
socket socket
|> assign_defaults(session) |> assign_defaults(session)
|> assign(:custom_shift, @custom_shift) |> assign(:custom_shift, @custom_shift)
|> assign(:show_template_btn_active, :false) |> assign(:show_template_btn_active, false)
|> assign(:show_template_details, :false) |> assign(:show_template_details, false)
|> assign(:selected_days, []) |> assign(:selected_days, [])
|> assign(:delete_days_shifts, nil)
|> live_okreply() |> live_okreply()
end end
@ -32,12 +33,18 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
|> show_details_if_custom() |> show_details_if_custom()
|> assign_shift_length() |> assign_shift_length()
|> assign_shift_template_changeset() |> assign_shift_template_changeset()
|> assign_modal_close_handlers()
|> init_today(Timex.today()) |> init_today(Timex.today())
|> init_calendar() |> init_calendar()
|> assign_known_shifts() |> assign_known_shifts()
|> live_noreply() |> live_noreply()
end end
defp assign_modal_close_handlers(socket) do
to = Routes.shift_assign_index_path(socket, :index)
assign(socket, modal_return_to: to, modal_close_action: :return)
end
defp get_shift_template("custom-shift"), do: @custom_shift defp get_shift_template("custom-shift"), do: @custom_shift
defp get_shift_template(template_id), do: Templates.get_shift_template(template_id) defp get_shift_template(template_id), do: Templates.get_shift_template(template_id)
@ -56,11 +63,11 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
defp init_calendar(%{assigns: %{current_user: user}} = socket) do defp init_calendar(%{assigns: %{current_user: user}} = socket) do
days = day_names(user.week_start_at) days = day_names(user.week_start_at)
{first, last, rows} = week_rows(socket.assigns.cursor_date, 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]) assign(socket, day_names: days, week_rows: rows, day_first: first, day_last: last)
end end
defp init_today(socket, today) do defp init_today(socket, today) do
assign(socket, [current_date: today, cursor_date: today]) assign(socket, current_date: today, cursor_date: today)
end end
defp assign_shift_template_changeset(%{assigns: %{shift_template: shift}} = socket) do defp assign_shift_template_changeset(%{assigns: %{shift_template: shift}} = socket) do
@ -71,7 +78,7 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
defp init_shift_template(socket) do defp init_shift_template(socket) do
first_list_id = socket.assigns.shift_templates |> hd() |> elem(1) first_list_id = socket.assigns.shift_templates |> hd() |> elem(1)
fave_id = socket.assigns.current_user.fave_shift_template_id fave_id = socket.assigns.current_user.fave_shift_template_id
assign_shift_template(socket, (fave_id || first_list_id)) assign_shift_template(socket, fave_id || first_list_id)
end end
defp assign_shift_template(socket, template_id) do defp assign_shift_template(socket, template_id) do
@ -83,14 +90,17 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
Templates.list_shift_templates_by_user_id(user.id) Templates.list_shift_templates_by_user_id(user.id)
|> Enum.map(fn t -> shift_template_option(t, user.fave_shift_template_id) end) |> Enum.map(fn t -> shift_template_option(t, user.fave_shift_template_id) end)
|> Enum.concat([@custom_shift_opt]) |> Enum.concat([@custom_shift_opt])
assign(socket, :shift_templates, shift_templates) assign(socket, :shift_templates, shift_templates)
end end
defp shift_template_option(template, fave_id) do defp shift_template_option(template, fave_id) do
label = label =
template.subject <> " (" <> template.subject <>
format_shift_time(template.time_start) <> "" <> " (" <>
format_shift_time(template.time_end) <> ")" format_shift_time(template.time_start) <>
"" <>
format_shift_time(template.time_end) <> ")"
label = label =
case fave_id == template.id do case fave_id == template.id do
@ -126,7 +136,7 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
week_rows = week_rows =
Interval.new(from: first, until: last, right_open: false) Interval.new(from: first, until: last, right_open: false)
|> Enum.map(& NaiveDateTime.to_date(&1)) |> Enum.map(&NaiveDateTime.to_date(&1))
|> Enum.chunk_every(7) |> Enum.chunk_every(7)
{first, last, week_rows} {first, last, week_rows}
@ -141,11 +151,14 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
true -> "bg-triangle-white" true -> "bg-triangle-white"
end end
Timex.compare(day, current_date, :days) == 0 -> "bg-info text-white" Timex.compare(day, current_date, :days) == 0 ->
"bg-info text-white"
day.month != cursor_date.month -> "bg-light text-gray" day.month != cursor_date.month ->
"bg-light text-gray"
true -> "" true ->
""
end end
end end
@ -157,11 +170,12 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
end end
defp show_details_if_custom(socket) do defp show_details_if_custom(socket) do
if (socket.assigns.shift_template.id != @custom_shift.id) || socket.assigns.show_template_details do if socket.assigns.shift_template.id != @custom_shift.id ||
socket.assigns.show_template_details do
socket socket
else else
socket socket
|> assign(:show_template_btn_active, :true) |> assign(:show_template_btn_active, true)
|> push_event("toggle-template-details", %{targetId: "#templateDetailsCol"}) |> push_event("toggle-template-details", %{targetId: "#templateDetailsCol"})
end end
end end
@ -197,7 +211,11 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
end end
@impl true @impl true
def handle_event("change-selected-template", %{"template_select" => %{"template" => template_id}}, socket) do def handle_event(
"change-selected-template",
%{"template_select" => %{"template" => template_id}},
socket
) do
socket socket
|> assign_shift_template(template_id) |> assign_shift_template(template_id)
|> show_details_if_custom() |> show_details_if_custom()
@ -210,9 +228,11 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
def handle_event("month-nav", %{"month" => direction}, socket) do def handle_event("month-nav", %{"month" => direction}, socket) do
new_cursor = new_cursor =
cond do cond do
direction == "now" -> Timex.today() direction == "now" ->
Timex.today()
true -> true ->
months = direction == "prev" && -1 || 1 months = (direction == "prev" && -1) || 1
Timex.shift(socket.assigns.cursor_date, months: months) Timex.shift(socket.assigns.cursor_date, months: months)
end end
@ -229,12 +249,12 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
@impl true @impl true
def handle_event("collapse-shown", %{"target_id" => _target_id}, socket) do def handle_event("collapse-shown", %{"target_id" => _target_id}, socket) do
{:noreply, assign(socket, :show_template_details, :true)} {:noreply, assign(socket, :show_template_details, true)}
end end
@impl true @impl true
def handle_event("collapse-hidden", %{"target_id" => _target_id}, socket) do def handle_event("collapse-hidden", %{"target_id" => _target_id}, socket) do
{:noreply, assign(socket, :show_template_details, :false)} {:noreply, assign(socket, :show_template_details, false)}
end end
@impl true @impl true
@ -248,6 +268,14 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
{:noreply, assign(socket, :selected_days, selected_days)} {:noreply, assign(socket, :selected_days, selected_days)}
end end
@impl true
def handle_event("delete-days-shifts", _params, socket) do
socket
|> assign(:modal_close_action, :delete_days_shifts)
|> assign(:delete_days_shifts, socket.assigns.selected_days)
|> live_noreply()
end
@impl true @impl true
def handle_event("clear-days", _params, socket) do def handle_event("clear-days", _params, socket) do
{:noreply, assign(socket, :selected_days, [])} {:noreply, assign(socket, :selected_days, [])}
@ -259,7 +287,8 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
shift_data = shift_data_from_template(socket.assigns.shift_template) shift_data = shift_data_from_template(socket.assigns.shift_template)
# 2. create list of shift attrs to insert # 2. create list of shift attrs to insert
to_insert = Enum.map(socket.assigns.selected_days, &shift_from_day_and_shift_data(&1, shift_data)) to_insert =
Enum.map(socket.assigns.selected_days, &shift_from_day_and_shift_data(&1, shift_data))
# 3. insert the data # 3. insert the data
{status, msg} = insert_shifts(to_insert, length(socket.assigns.selected_days)) {status, msg} = insert_shifts(to_insert, length(socket.assigns.selected_days))
@ -271,10 +300,37 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
|> live_noreply() |> live_noreply()
end end
@impl true
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
socket |> put_flash(flash_type, msg) |> live_noreply()
end
@impl true
def handle_info({:clear_selected_days, _}, socket) do
socket |> assign(:selected_days, []) |> live_noreply()
end
@impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
socket
|> copy_flash()
|> push_patch(to: socket.assigns.modal_return_to)
|> live_noreply()
end
@impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
socket
|> assign(assign_key, nil)
|> assign_modal_close_handlers()
|> assign_known_shifts()
|> live_noreply()
end
defp shift_data_from_template(shift_template) do defp shift_data_from_template(shift_template) do
shift_template shift_template
|> Map.from_struct() |> Map.from_struct()
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user]) |> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
end end
defp shift_from_day_and_shift_data(day, shift_data) do defp shift_from_day_and_shift_data(day, shift_data) do
@ -289,12 +345,16 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{insert_all: {n, _}}} -> {:ok, %{insert_all: {n, _}}} ->
s = (n > 1 && "s") || ""
if n == day_count do if n == day_count do
{:success, "Successfully assigned shift to #{n} day(s)"} {:success, "Successfully assigned shift to #{n} day#{s}"}
else else
{:warning, "Some error, only #{n} day(s) inserted but #{day_count} were selected"} {:warning, "Some error, only #{n} day#{s} inserted but #{day_count} were selected"}
end end
_ -> {:error, "Ope, unknown error inserting shifts, page the dev"}
_ ->
{:error, "Ope, unknown error inserting shifts, page the dev"}
end end
end end

View file

@ -1,3 +1,13 @@
<%= if @delete_days_shifts do %>
<%= live_modal @socket, Shift73kWeb.ShiftAssignLive.DeleteComponent,
id: "delete-days-shifts-#{@current_user.id}",
title: "Delete Shifts From Selected Days",
delete_days_shifts: @delete_days_shifts,
current_user: @current_user
%>
<% end %>
<h2 class="mb-3 mb-sm-0"> <h2 class="mb-3 mb-sm-0">
<%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %> <%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %>
Assign Shift To Dates Assign Shift To Dates
@ -223,17 +233,22 @@
</table> </table>
<div class="row justify-content-end my-4"> <div class="row justify-content-center justify-content-lg-end my-5">
<div class="col-auto"> <div class="col-12 col-sm-10 col-md-8 col-lg-auto d-flex flex-column-reverse flex-lg-row">
<button class="btn btn-outline-dark" phx-click="clear-days"> <button class="btn btn-outline-danger mb-1 mb-lg-0 me-lg-1" phx-click="delete-days-shifts" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
<%= icon_div @socket, "bi-eraser", [class: "icon baseline"] %> <%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
Clear Delete shifts from selected days
</button> </button>
<button class="btn btn-primary" phx-click="save-days" <%= if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: "disabled" %>> <button class="btn btn-outline-dark mb-1 mb-lg-0 me-lg-1" phx-click="clear-days" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
<%= icon_div @socket, "bi-eraser", [class: "icon baseline"] %>
De-select all selected
</button>
<button class="btn btn-primary mb-1 mb-lg-0" phx-click="save-days" <%= if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: "disabled" %>>
<%= icon_div @socket, "bi-save", [class: "icon baseline"] %> <%= icon_div @socket, "bi-save", [class: "icon baseline"] %>
Save assigned shifts Assign shifts to selected days
</button> </button>
</div> </div>

View file

@ -11,11 +11,6 @@ defmodule Shift73kWeb.ShiftTemplateLive.DeleteComponent do
|> live_okreply() |> live_okreply()
end end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
@impl true @impl true
def handle_event("confirm", %{"id" => id, "subject" => subject}, socket) do def handle_event("confirm", %{"id" => id, "subject" => subject}, socket) do
id id

View file

@ -8,7 +8,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn me-2", phx_click: "cancel", phx_target: @myself %> <%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
<%= link "Confirm Delete", to: "#", <%= link "Confirm Delete", to: "#",
class: "btn btn-danger", class: "btn btn-danger",
phx_click: "confirm", phx_click: "confirm",

View file

@ -52,11 +52,6 @@ defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
) )
end end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
defp save_shift_template(socket, :new, params) do defp save_shift_template(socket, :new, params) do
case Templates.create_shift_template(params) do case Templates.create_shift_template(params) do
{:ok, _shift_template} -> {:ok, _shift_template} ->

View file

@ -104,7 +104,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn", phx_click: "cancel", phx_target: @myself %> <%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
<%= submit "Save", <%= submit "Save",
class: "btn btn-primary ", class: "btn btn-primary ",
disabled: !@changeset.valid?, disabled: !@changeset.valid?,

View file

@ -23,7 +23,7 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do
if Roles.can?(current_user, shift_template, live_action) do if Roles.can?(current_user, shift_template, live_action) do
socket socket
|> assign_shift_templates() |> assign_shift_templates()
|> assign(:modal_return_to, Routes.shift_template_index_path(socket, :index)) |> assign_modal_close_handlers()
|> assign(:delete_shift_template, nil) |> assign(:delete_shift_template, nil)
|> apply_action(socket.assigns.live_action, params) |> apply_action(socket.assigns.live_action, params)
|> live_noreply() |> live_noreply()
@ -35,6 +35,11 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do
end end
end end
defp assign_modal_close_handlers(socket) do
to = Routes.shift_template_index_path(socket, :index)
assign(socket, modal_return_to: to, modal_close_action: :return)
end
defp apply_action(socket, :clone, %{"id" => id}) do defp apply_action(socket, :clone, %{"id" => id}) do
socket socket
|> assign(:page_title, "Clone Shift Template") |> assign(:page_title, "Clone Shift Template")
@ -74,9 +79,13 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do
@impl true @impl true
def handle_event("delete-modal", %{"id" => id}, socket) do def handle_event("delete-modal", %{"id" => id}, socket) do
{:noreply, assign(socket, :delete_shift_template, Templates.get_shift_template!(id))} socket
|> assign(:modal_close_action, :delete_shift_template)
|> assign(:delete_shift_template, Templates.get_shift_template!(id))
|> live_noreply()
end end
@impl true
def handle_event("set-user-fave-shift-template", %{"id" => shift_template_id}, socket) do def handle_event("set-user-fave-shift-template", %{"id" => shift_template_id}, socket) do
user_id = socket.assigns.current_user.id user_id = socket.assigns.current_user.id
Accounts.set_user_fave_shift_template(user_id, shift_template_id) Accounts.set_user_fave_shift_template(user_id, shift_template_id)
@ -87,6 +96,7 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do
|> live_noreply() |> live_noreply()
end end
@impl true
def handle_event("unset-user-fave-shift-template", _params, socket) do def handle_event("unset-user-fave-shift-template", _params, socket) do
user_id = socket.assigns.current_user.id user_id = socket.assigns.current_user.id
Accounts.unset_user_fave_shift_template(user_id) Accounts.unset_user_fave_shift_template(user_id)
@ -98,13 +108,24 @@ defmodule Shift73kWeb.ShiftTemplateLive.Index do
end end
@impl true @impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
socket |> copy_flash() |> push_patch(to: to) |> live_noreply() socket
|> copy_flash()
|> push_patch(to: socket.assigns.modal_return_to)
|> live_noreply()
end
@impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
socket
|> assign(assign_key, nil)
|> assign_modal_close_handlers()
|> assign_shift_templates()
|> live_noreply()
end end
@impl true @impl true
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
socket |> put_flash(flash_type, msg) |> live_noreply() socket |> put_flash(flash_type, msg) |> live_noreply()
end end
end end

View file

@ -11,8 +11,7 @@
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent, <%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent,
id: @delete_shift_template.id, id: @delete_shift_template.id,
title: "Delete Shift Template", title: "Delete Shift Template",
delete_shift_template: @delete_shift_template, delete_shift_template: @delete_shift_template %>
current_user: @current_user %>
<% end %> <% end %>

View file

@ -5,14 +5,7 @@ defmodule Shift73kWeb.UserManagement.DeleteComponent do
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
socket socket |> assign(assigns) |> live_okreply()
|> assign(assigns)
|> live_okreply()
end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end end
@impl true @impl true

View file

@ -5,7 +5,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn me-2", phx_click: "cancel", phx_target: @myself %> <%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
<%= link "Confirm Delete", to: "#", <%= link "Confirm Delete", to: "#",
class: "btn btn-danger", class: "btn btn-danger",
phx_click: "confirm", phx_click: "confirm",

View file

@ -80,11 +80,6 @@ defmodule Shift73kWeb.UserManagement.FormComponent do
save_user(socket, user_params) save_user(socket, user_params)
end end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
def role_description(role) when is_atom(role) do def role_description(role) when is_atom(role) do
Keyword.get(User.roles(), role) Keyword.get(User.roles(), role)
end end

View file

@ -49,7 +49,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn", phx_click: "cancel", phx_target: @myself %> <%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
<%= submit "Save", <%= submit "Save",
class: "btn btn-primary ", class: "btn btn-primary ",
disabled: !@changeset.valid?, disabled: !@changeset.valid?,

View file

@ -28,9 +28,8 @@ defmodule Shift73kWeb.UserManagementLive.Index do
if Roles.can?(current_user, user, live_action) do if Roles.can?(current_user, user, live_action) do
socket socket
|> assign(:query, query_map(params)) |> assign(:query, query_map(params))
|> assign_modal_return_to() |> assign_modal_close_handlers()
|> assign(:delete_user, nil) |> assign(:delete_user, nil)
|> assign(:page, nil)
|> request_page_query() |> request_page_query()
|> apply_action(socket.assigns.live_action, params) |> apply_action(socket.assigns.live_action, params)
|> live_noreply() |> live_noreply()
@ -65,9 +64,9 @@ defmodule Shift73kWeb.UserManagementLive.Index do
|> assign(:user, Accounts.get_user!(id)) |> assign(:user, Accounts.get_user!(id))
end end
def assign_modal_return_to(%{assigns: %{query: query}} = socket) do defp assign_modal_close_handlers(%{assigns: %{query: query}} = socket) do
to = Routes.user_management_index_path(socket, :index, Enum.into(query, [])) to = Routes.user_management_index_path(socket, :index, Enum.into(query, []))
assign(socket, :modal_return_to, to) assign(socket, modal_return_to: to, modal_close_action: :return)
end end
defp user_from_params(params) defp user_from_params(params)
@ -98,7 +97,10 @@ defmodule Shift73kWeb.UserManagementLive.Index do
@impl true @impl true
def handle_event("delete-modal", %{"id" => id}, socket) do def handle_event("delete-modal", %{"id" => id}, socket) do
{:noreply, assign(socket, :delete_user, Accounts.get_user!(id))} socket
|> assign(:modal_close_action, :delete_user)
|> assign(:delete_user, Accounts.get_user!(id))
|> live_noreply()
end end
@impl true @impl true
@ -114,17 +116,13 @@ defmodule Shift73kWeb.UserManagementLive.Index do
end end
@impl true @impl true
def handle_event( def handle_event("sort-change", %{"sort_by" => column} = params, socket) do
"sort-change", if column == socket.assigns.query.sort_by do
%{"sort_by" => column} = params, order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc"
%{assigns: %{query: query}} = socket send(self(), {:query_update, %{"sort_order" => order}})
) do else
(column == query.sort_by &&
send(
self(),
{:query_update, %{"sort_order" => (query.sort_order == "asc" && "desc") || "asc"}}
)) ||
send(self(), {:query_update, Map.put(params, "sort_order", "asc")}) send(self(), {:query_update, Map.put(params, "sort_order", "asc")})
end
{:noreply, socket} {:noreply, socket}
end end
@ -169,8 +167,20 @@ defmodule Shift73kWeb.UserManagementLive.Index do
end end
@impl true @impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
socket |> copy_flash() |> push_patch(to: to) |> live_noreply() socket
|> copy_flash()
|> push_patch(to: socket.assigns.modal_return_to)
|> live_noreply()
end
@impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
socket
|> assign(assign_key, nil)
|> assign_modal_close_handlers()
|> request_page_query()
|> live_noreply()
end end
@impl true @impl true

View file

@ -11,8 +11,8 @@
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent, <%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
id: @delete_user.id, id: @delete_user.id,
title: "Delete User", title: "Delete User",
delete_user: @delete_user, delete_user: @delete_user
current_user: @current_user %> %>
<% end %> <% end %>

View file

@ -0,0 +1,36 @@
defmodule Shift73kWeb.Redirector do
import Phoenix.Controller, only: [redirect: 2]
def init([to: _] = opts), do: opts
def init([external: _] = opts), do: opts
def init(_default), do: raise("Missing required to: / external: option in redirect")
def call(conn, [to: to]) do
redirect(conn, to: append_query_string(conn, to))
end
def call(conn, [external: url]) do
external = url
|> URI.parse
|> merge_query_string(conn)
|> URI.to_string
redirect(conn, external: external)
end
defp append_query_string(%Plug.Conn{query_string: ""}, path), do: path
defp append_query_string(%Plug.Conn{query_string: query}, path), do: "#{path}?#{query}"
defp merge_query_string(%URI{query: nil} = destination_uri, %Plug.Conn{query_string: source}) do
%{destination_uri | query: source}
end
defp merge_query_string(%URI{query: destination} = destination_uri, %Plug.Conn{query_string: source}) do
merged_query = Map.merge(
URI.decode_query(destination),
URI.decode_query(source)
)
%{destination_uri | query: URI.encode_query(merged_query)}
end
end

View file

@ -32,8 +32,7 @@ defmodule Shift73kWeb.Router do
scope "/", Shift73kWeb do scope "/", Shift73kWeb do
pipe_through([:browser]) pipe_through([:browser])
live("/", PageLive, :index) get("/", Redirector, to: "/assign")
get("/other", OtherController, :index)
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.

View file

@ -2,7 +2,7 @@
<div class="container"> <div class="container">
<h1 class="fs-4 my-0 py-0 lh-base"> <h1 class="fs-4 my-0 py-0 lh-base">
<%= link to: Routes.page_path(@conn, :index), class: "navbar-brand fs-4" do %> <%= link to: "/", class: "navbar-brand fs-4" do %>
<%= icon_div @conn, "bi-calendar2-week", [class: "icon baseline me-1"] %> <%= icon_div @conn, "bi-calendar2-week", [class: "icon baseline me-1"] %>
<span class="fw-light">Shift73k</span> <span class="fw-light">Shift73k</span>
<% end %> <% end %>
@ -26,14 +26,14 @@
<%# nav LEFT items %> <%# nav LEFT items %>
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<%= if @current_user do %> <%#= if @current_user do %>
<li class="nav-item"> <%# <li class="nav-item"> %>
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "nav-link") do %> <%#= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "nav-link") do %>
<%= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %> <%#= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
Templates <%# Templates %>
<% end %> <%# end %>
</li> <%# </li> %>
<% end %> <%# end %>
<%# normal navbar link example %> <%# normal navbar link example %>
<%# <li class="nav-item"> %> <%# <li class="nav-item"> %>
@ -66,13 +66,19 @@
<%# nav RIGHT items %> <%# nav RIGHT items %>
<ul class="navbar-nav"> <ul class="navbar-nav">
<%= render "navbar/_user_menu.html", assigns %> <%= if @current_user do %>
<%= render "navbar/_shifts_menu.html", assigns %>
<%= render "navbar/_user_menu.html", assigns %>
<% else %>
<%= if !@current_user do %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-none d-lg-block") do %> <%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-none d-lg-block") do %>
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %> <%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
Log in Log in
<% end %> <% end %>
<% end %> <% end %>
</ul> </ul>

View file

@ -0,0 +1,25 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline me-1"] %>
Shifts
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
<li>
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
<%= icon_div @conn, "bi-calendar2-plus", [class: "icon baseline me-1"] %>
Assign Shift To Dates
<% end %>
</li>
<li>
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
<%= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
My Shift Templates
<% end %>
</li>
</ul>
</li>

View file

@ -1,5 +1,3 @@
<%= if @current_user do %>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
@ -36,5 +34,3 @@
</ul> </ul>
</li> </li>
<% end %>

View file

@ -1,41 +0,0 @@
<h1 class="text-3xl font-bold leading-tight text-gunmetal-200">
Other Page
</h1>
<h2 class="text-xl leading-tight text-gunmetal-400">
With a subtitle no less!
</h2>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<!-- Replace with your content -->
<%# <div class="px-4 py-6 sm:px-0">
<div class="border-4 border-dashed border-gray-200 rounded-lg h-96"></div>
</div> %>
<p>
Praesent velit justo, auctor ut nibh id, fermentum eleifend nunc. Cras sed purus dignissim, ornare elit et, ultrices elit. Sed quis neque consequat, laoreet ante a, hendrerit elit. Maecenas dapibus sed nulla vitae consectetur. Duis sollicitudin augue nisl, et rhoncus enim tempor at. Fusce scelerisque sollicitudin purus sit amet iaculis. Phasellus lacinia mi ut laoreet accumsan. Sed sagittis erat nec sem placerat, ut volutpat neque porttitor. Suspendisse tempor mauris vel mollis sagittis. In ut laoreet arcu. Duis sed felis in dui imperdiet luctus nec faucibus sem. Donec commodo urna ut enim fringilla, quis lacinia ligula malesuada. Quisque feugiat fermentum pretium. Integer sed porttitor lacus, sed bibendum diam. Aliquam dapibus neque et pharetra interdum.
</p>
<!-- /End replace -->
</div>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
Launch demo modal
</button>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Aliquam ultrices elit purus, eget dignissim orci pulvinar id. Curabitur tincidunt, ligula eu condimentum porttitor, nibh sapien scelerisque urna, nec cursus nisi nisi a neque. Mauris hendrerit orci blandit, suscipit ante nec, porttitor neque. Nunc.
</div>
<div class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>

View file

@ -1,3 +0,0 @@
defmodule Shift73kWeb.OtherView do
use Shift73kWeb, :view
end

View file

@ -10,7 +10,7 @@ defmodule Shift73k.Repo.Migrations.CreateShiftTemplates do
add :time_zone, :string, null: false add :time_zone, :string, null: false
add :time_start, :time, null: false add :time_start, :time, null: false
add :time_end, :time, null: false add :time_end, :time, null: false
add :user_id, references(:users, on_delete: :nothing, type: :binary_id) add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
timestamps() timestamps()
end end

View file

@ -3,7 +3,7 @@ defmodule Shift73k.Repo.Migrations.AddUserDefaultShiftColumn do
def change do def change do
alter table(:users) do alter table(:users) do
add(:fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :delete_all)) add(:fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :nilify_all))
end end
end end
end end

View file

@ -1,66 +0,0 @@
defmodule Shift73kWeb.PageLiveTest do
use Shift73kWeb.ConnCase
import Phoenix.LiveViewTest
import Shift73k.AccountsFixtures
test "disconnected and connected render with authentication should redirect to index page", %{
conn: conn
} do
conn = conn |> log_in_user(user_fixture())
{:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Welcome to Phoenix!"
assert render(page_live) =~ "Welcome to Phoenix!"
end
test "logs out when force logout on logged user", %{
conn: conn
} do
user = user_fixture()
conn = conn |> log_in_user(user)
{:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Welcome to Phoenix!"
assert render(page_live) =~ "Welcome to Phoenix!"
Shift73k.Accounts.logout_user(user)
# Assert our liveview process is down
ref = Process.monitor(page_live.pid)
assert_receive {:DOWN, ^ref, _, _, _}
refute Process.alive?(page_live.pid)
# Assert our liveview was redirected, following first to /users/force_logout, then to "/", and then to "/users/log_in"
assert_redirect(page_live, "/users/force_logout")
conn = get(conn, "/users/force_logout")
assert "/" = redir_path = redirected_to(conn, 302)
conn = get(recycle(conn), redir_path)
assert html_response(conn, 200) =~
"You were logged out. Please login again to continue using our application."
end
test "doesn't log out when force logout on another user", %{
conn: conn
} do
user1 = user_fixture()
user2 = user_fixture()
conn = conn |> log_in_user(user2)
{:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Welcome to Phoenix!"
assert render(page_live) =~ "Welcome to Phoenix!"
Shift73k.Accounts.logout_user(user1)
# Assert our liveview is alive
ref = Process.monitor(page_live.pid)
refute_receive {:DOWN, ^ref, _, _, _}
assert Process.alive?(page_live.pid)
# If we are able to rerender the page it means nothing happened
assert render(page_live) =~ "Welcome to Phoenix!"
end
end