shift template live crud working

This commit is contained in:
Adam Piontek 2021-03-11 13:30:30 -05:00
parent 9e8ecb8b89
commit 8aa4b8eee0
14 changed files with 9279 additions and 1123 deletions

View file

@ -22,8 +22,8 @@ defmodule Shift73k.ShiftTemplates do
end
def list_shift_templates_by_user_id(user_id) do
from(s in ShiftTemplate, where: s.user_id == ^user_id)
|> Repo.all()
q = from s in ShiftTemplate, where: s.user_id == ^user_id, order_by: s.subject
Repo.all(q)
end
@doc """

View file

@ -8,13 +8,12 @@ defmodule Shift73k.ShiftTemplates.ShiftTemplate do
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "shift_templates" do
field :subject, :string, default: "My Work Shift"
field :subject, :string
field :description, :string
field :location, :string
field :timezone, :string, default: @time_zone
field :start_time, :time, default: ~T[09:00:00]
field :length_hours, :integer, default: 8
field :length_minutes, :integer, default: 0
field :time_zone, :string, default: @time_zone
field :time_start, :time, default: ~T[09:00:00]
field :time_end, :time, default: ~T[17:00:00]
belongs_to(:user, Shift73k.Accounts.User)
@ -26,22 +25,61 @@ defmodule Shift73k.ShiftTemplates.ShiftTemplate do
shift_template
|> cast(attrs, [
:subject,
:description,
:location,
:timezone,
:start_time,
:length_hours,
:length_minutes,
:description,
:time_zone,
:time_start,
:time_end,
:user_id
])
|> validate_required([
:subject,
:timezone,
:start_time,
:length_hours
:time_zone,
:time_start,
:time_end
])
|> validate_number(:length_hours, greater_than_or_equal_to: 0, less_than_or_equal_to: 24)
|> validate_number(:length_minutes, greater_than_or_equal_to: 0, less_than: 60)
|> validate_inclusion(:timezone, Tzdata.zone_list())
|> validate_length(:subject, count: :codepoints, max: 280)
|> validate_length(:location, count: :codepoints, max: 280)
|> validate_change(:time_end, fn :time_end, time_end ->
shift_length = shift_length(get_time_start(attrs), time_end)
cond do
shift_length == 0 ->
[time_end: "end time cannot equal start time"]
shift_length >= 16 * 3600 ->
[time_end: "you don't want to work 16 or more hours!"]
true ->
[]
end
end)
|> validate_inclusion(:time_zone, Tzdata.zone_list())
end
defp get_time_start(%{"time_start" => time_start}), do: time_start
defp get_time_start(%{time_start: time_start}), do: time_start
defp get_time_start(_), do: nil
def shift_length(time_start, time_end) do
cond do
time_end > time_start ->
Time.diff(time_end, time_start)
time_start > time_end ->
len1 = Time.diff(~T[23:59:59], time_start) + 1
len2 = Time.diff(time_end, ~T[00:00:00])
len1 + len2
true ->
0
end
end
def shift_length_h_m_tuple(time_start, time_end) do
shift_length_seconds = shift_length(time_start, time_end)
h = shift_length_seconds |> Integer.floor_div(3600)
m = shift_length_seconds |> rem(3600) |> Integer.floor_div(60)
{h, m}
end
end

View file

@ -1,13 +1,9 @@
<div class="modal-body">
Are you sure you want to delete "<%= @delete_shift_template.subject %>
(<%= @delete_shift_template.start_time |> Calendar.strftime("%I:%M%P") %>
(<%= @delete_shift_template.time_start |> Calendar.strftime("%I:%M%P") %>
&mdash;
<%=
@delete_shift_template.start_time
|> Time.add((60 * 60 * @delete_shift_template.length_hours) + ((@delete_shift_template.length_minutes || 0) * 60))
|> Calendar.strftime("%I:%M%P")
%>)"?
<%= @delete_shift_template.time_end |> Calendar.strftime("%I:%M%P") %>)"?
</div>
<div class="modal-footer">

View file

@ -2,54 +2,96 @@ defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
use Shift73kWeb, :live_component
alias Shift73k.ShiftTemplates
alias Shift73k.ShiftTemplates.ShiftTemplate
@impl true
def update(%{shift_template: shift_template} = assigns, socket) do
changeset = ShiftTemplates.change_shift_template(shift_template)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
socket
|> assign(assigns)
|> assign(:changeset, changeset)
|> assign_shift_length(shift_template.time_start, shift_template.time_end)
|> live_okreply()
end
defp assign_shift_length(socket, time_start, time_end) do
shift_length = ShiftTemplate.shift_length_h_m_tuple(time_start, time_end)
assign(socket, :shift_length, shift_length)
end
defp prep_shift_template_params(shift_template_params, current_user) do
time_start = Time.from_iso8601!("T#{shift_template_params["time_start"]}:00")
time_end = Time.from_iso8601!("T#{shift_template_params["time_end"]}:00")
shift_template_params
|> Map.put("time_start", time_start)
|> Map.put("time_end", time_end)
|> Map.put("user_id", current_user.id)
end
@impl true
def handle_event("validate", %{"shift_template" => shift_template_params}, socket) do
shift_template_params =
prep_shift_template_params(shift_template_params, socket.assigns.current_user)
changeset =
socket.assigns.shift_template
|> ShiftTemplates.change_shift_template(shift_template_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
socket
|> assign(:changeset, changeset)
|> assign_shift_length(
shift_template_params["time_start"],
shift_template_params["time_end"]
)
|> live_noreply()
end
def handle_event("save", %{"shift_template" => shift_template_params}, socket) do
save_shift_template(socket, socket.assigns.action, shift_template_params)
save_shift_template(
socket,
socket.assigns.action,
prep_shift_template_params(shift_template_params, socket.assigns.current_user)
)
end
defp save_shift_template(socket, :edit, shift_template_params) do
case ShiftTemplates.update_shift_template(socket.assigns.shift_template, shift_template_params) do
{:ok, _shift_template} ->
{:noreply,
socket
|> put_flash(:info, "Shift template updated successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
defp save_shift_template(socket, :new, shift_template_params) do
case ShiftTemplates.create_shift_template(shift_template_params) do
{:ok, _shift_template} ->
{:noreply,
socket
|> put_flash(:info, "Shift template created successfully")
|> push_redirect(to: socket.assigns.return_to)}
flash = {:info, "Shift template created successfully"}
send(self(), {:put_flash_message, flash})
socket
|> push_event("modal-please-hide", %{})
|> live_noreply()
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
defp save_shift_template(socket, :edit, shift_template_params) do
case ShiftTemplates.update_shift_template(
socket.assigns.shift_template,
shift_template_params
) do
{:ok, _shift_template} ->
flash = {:info, "Shift template updated successfully"}
send(self(), {:put_flash_message, flash})
socket
|> push_event("modal-please-hide", %{})
|> live_noreply()
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end

View file

@ -6,39 +6,107 @@
<div class="modal-body">
<%= label f, :subject, "Subject/Title", class: "form-label" %>
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :subject) %>">
<%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %>
<%= text_input f, :subject,
value: input_value(f, :subject),
class: input_class(f, :subject, "form-control"),
autofocus: true,
aria_describedby: error_ids(f, :subject)
%>
<%= error_tag f, :subject %>
</div>
<%= label f, :subject %>
<%= text_input f, :subject %>
<%= error_tag f, :subject %>
<div class="row gx-2 gx-sm-3">
<%= label f, :description %>
<%= text_input f, :description %>
<%= error_tag f, :description %>
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_start) %>">
<%= label f, :time_start, "Start", class: "form-label" %>
<%= time_input f, :time_start,
precision: :minute,
value: input_value(f, :time_start),
class: input_class(f, :time_start, "form-control"),
aria_describedby: error_ids(f, :time_start)
%>
</div>
<%= label f, :location %>
<%= text_input f, :location %>
<%= error_tag f, :location %>
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_end) %>">
<%= label f, :time_end, "End", class: "form-label" %>
<%= time_input f, :time_end,
precision: :minute,
value: input_value(f, :time_end),
class: input_class(f, :time_end, "form-control"),
aria_describedby: error_ids(f, :time_end)
%>
</div>
<%= label f, :timezone %>
<%= text_input f, :timezone %>
<%= error_tag f, :timezone %>
</div>
<%= label f, :start_time %>
<%= time_select f, :start_time %>
<%= error_tag f, :start_time %>
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length |> elem(0) %>h <%= @shift_length |> elem(1) %>m</div>
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_start) %>">
<%= error_tag f, :time_start %>
</div>
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_end) %>">
<%= error_tag f, :time_end %>
</div>
<%= label f, :location, class: "form-label mt-3" %>
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :location) %>">
<%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %>
<%= text_input f, :location,
value: input_value(f, :location),
class: input_class(f, :location, "form-control"),
aria_describedby: error_ids(f, :location)
%>
<%= error_tag f, :location %>
</div>
<%= label f, :description, class: "form-label" %>
<div class="mb-3" phx-feedback-for="<%= input_id(f, :description) %>">
<%= textarea f, :description,
value: input_value(f, :description),
class: input_class(f, :description, "form-control"),
aria_describedby: error_ids(f, :description)
%>
<%= error_tag f, :description %>
</div>
<%= label f, :time_zone, class: "form-label" %>
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :time_zone) %>">
<%= 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",
list: "tz_list"
%>
<datalist id="tz_list">
<%= for tz_name <- Tzdata.zone_list() do %>
<option value="<%= tz_name %>"></option>
<% 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>
<%= error_tag f, :time_zone %>
</div>
<%= label f, :length_hours %>
<%= number_input f, :length_hours %>
<%= error_tag f, :length_hours %>
<%= label f, :length_minutes %>
<%= number_input f, :length_minutes %>
<%= error_tag f, :length_minutes %>
</div>
<div class="modal-footer">
<%= submit "Save", phx_disable_with: "Saving..." %>
<%= link "Cancel", to: "#", class: "btn", phx_click: "cancel", phx_target: @myself %>
<%= submit "Save",
class: "btn btn-primary ",
disabled: !@changeset.valid?,
aria_disabled: !@changeset.valid? && "true" || false,
phx_disable_with: "Saving..."
%>
</div>

View file

@ -17,7 +17,7 @@
<div class="row justify-content-start justify-content-sm-center">
<div class="col-md-12 col-lg-10 col-xxl-8">
<div class="col-md-10 col-xl-10">
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center">
<h2 class="mb-3 mb-sm-0">
@ -35,7 +35,7 @@
<%= for shift <- @shift_templates do %>
<div class="col-12 col-md-6 col-xl-4 ">
<div class="col-12 col-lg-6">
<div class="card mt-4">
<h5 class="card-header">
@ -49,23 +49,25 @@
<tr>
<th scope="row" class="text-end">
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
<span class="visually-hidden">Hours</span>
<span class="visually-hidden">Hours:</span>
</th>
<td>
<%= shift.start_time |> Calendar.strftime("%I:%M%P") %>
<%= shift.time_start |> Calendar.strftime("%I:%M%P") %>
&mdash;
<%=
shift.start_time
|> Time.add((60 * 60 * shift.length_hours) + ((shift.length_minutes || 0) * 60))
|> Calendar.strftime("%I:%M%P")
%>
<%= shift.time_end |> Calendar.strftime("%I:%M%P") %>
<span class="text-muted">
<span class="visually-hidden">Shift length:</span>
<% shift_length = ShiftTemplate.shift_length_h_m_tuple(shift.time_start, shift.time_end) %>
(<%= shift_length |> elem(0) %>h <%= shift_length |> elem(1) %>m)
</span>
<span class="valid-feedback d-block text-muted mt-n1">TZ: <%= shift.time_zone %></span>
</td>
</tr>
<tr>
<th scope="row" class="text-end">
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
<span class="visually-hidden">Location</span>
<span class="visually-hidden">Location:</span>
</th>
<td>
<%= if shift.location do %>
@ -78,11 +80,11 @@
<tr>
<th scope="row" class="text-end">
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
<span class="visually-hidden">Description</span>
<span class="visually-hidden">Description:</span>
</th>
<td>
<%= if shift.description do %>
<%= shift.description %>
<%= text_to_html shift.description %>
<% else %>
<span class="text-muted fst-italic">none</span>
<% end %>

View file

@ -15,7 +15,7 @@
</span>
</button>
<% else %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-block d-lg-none") do %>
<%= icon_div @conn, "bi-door-open", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
Log in
<% end %>
@ -68,6 +68,13 @@
<%= render "navbar/_user_menu.html", assigns %>
<%= 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 %>
<%= icon_div @conn, "bi-door-open", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
Log in
<% end %>
<% end %>
</ul>
</div>