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

@ -2,9 +2,13 @@
/* hides the feedback field help if liveview indicates field not touched yet */
.phx-no-feedback.invalid-feedback,
.phx-no-feedback .invalid-feedback {
.phx-no-feedback .invalid-feedback,
.phx-orphaned-feedback.phx-no-feedback .invalid-feedback {
display: none;
}
.phx-orphaned-feedback .invalid-feedback {
display: block !important;
}
/* sets default bootstrap form-control styles if field not touched yet */
.phx-no-feedback .form-control.is-valid,

View file

@ -34,12 +34,14 @@ 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-event.svg"; // brand
import "../node_modules/bootstrap-icons/icons/clock-history.svg"; // brand
import "../node_modules/bootstrap-icons/icons/hourglass.svg"; // brand
import "../node_modules/bootstrap-icons/icons/geo.svg"; // brand
import "../node_modules/bootstrap-icons/icons/justify-left.svg"; // brand
import "../node_modules/bootstrap-icons/icons/plus-circle-dotted.svg"; // brand
import "../node_modules/bootstrap-icons/icons/calendar3-event.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";
import "../node_modules/bootstrap-icons/icons/map.svg";
import "../node_modules/bootstrap-icons/icons/geo.svg";
import "../node_modules/bootstrap-icons/icons/justify-left.svg";
import "../node_modules/bootstrap-icons/icons/plus-circle-dotted.svg";
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured

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>

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,12 @@ defmodule Shift73k.Repo.Migrations.CreateShiftTemplates do
def change do
create table(:shift_templates, primary_key: false) do
add :id, :binary_id, primary_key: true
add :subject, :string, null: false
add :description, :string
add :location, :string
add :timezone, :string, null: false
add :start_time, :time, null: false
add :length_hours, :integer, null: false
add :length_minutes, :integer
add :subject, :string, size: 280, null: false
add :location, :string, size: 280
add :description, :text
add :time_zone, :string, null: false
add :time_start, :time, null: false
add :time_end, :time, null: false
add :user_id, references(:users, on_delete: :nothing, type: :binary_id)
timestamps()

View file

@ -75,7 +75,6 @@ mock_users =
Repo.insert_all(User, mock_users)
# end
#####
# shift tepmlates
alias Shift73k.ShiftTemplates.ShiftTemplate
@ -101,15 +100,18 @@ for user <- Accounts.list_users() do
|> Enum.map(fn e ->
seconds_to_add = :rand.uniform(seconds_days_14) + seconds_half_day
add_dt = NaiveDateTime.add(user.inserted_at, seconds_to_add)
time_start = time_from_mock.(e["time_start"])
shift_len_min = e["length_minutes"] || 0
shift_length = e["length_hours"] * 60 * 60 + shift_len_min * 60
time_end = Time.add(time_start, shift_length) |> Time.truncate(:second)
%{
subject: e["subject"],
description: e["description"],
location: e["location"],
timezone: (Tzdata.zone_list() |> Enum.random()),
start_time: time_from_mock.(e["start_time"]),
length_hours: e["length_hours"],
length_minutes: e["length_minutes"],
time_zone: Tzdata.zone_list() |> Enum.random(),
time_start: time_start,
time_end: time_end,
user_id: user.id,
inserted_at: add_dt,
updated_at: add_dt
@ -117,5 +119,4 @@ for user <- Accounts.list_users() do
end)
Repo.insert_all(ShiftTemplate, user_shifts)
end

View file

@ -6,9 +6,9 @@ defmodule Shift73k.ShiftTemplatesTest do
describe "shift_templates" do
alias Shift73k.ShiftTemplates.ShiftTemplate
@valid_attrs %{description: "some description", label: "some label", length_hours: 42, length_minutes: 42, location: "some location", start_time: ~T[14:00:00], subject: "some subject", timezone: "some timezone"}
@update_attrs %{description: "some updated description", label: "some updated label", length_hours: 43, length_minutes: 43, location: "some updated location", start_time: ~T[15:01:01], subject: "some updated subject", timezone: "some updated timezone"}
@invalid_attrs %{description: nil, label: nil, length_hours: nil, length_minutes: nil, location: nil, start_time: nil, subject: nil, timezone: nil}
@valid_attrs %{description: "some description", location: "some location", time_start: ~T[08:00:00], time_end: ~T[16:00:00], subject: "some subject", time_zone: "some time_zone"}
@update_attrs %{description: "some updated description", location: "some updated location", time_start: ~T[13:00:00], time_end: ~T[19:30:00], subject: "some updated subject", time_zone: "some updated time_zone"}
@invalid_attrs %{description: nil, location: nil, time_start: nil, time_end: nil, subject: nil, time_zone: nil}
def shift_template_fixture(attrs \\ %{}) do
{:ok, shift_template} =
@ -32,12 +32,11 @@ defmodule Shift73k.ShiftTemplatesTest do
test "create_shift_template/1 with valid data creates a shift_template" do
assert {:ok, %ShiftTemplate{} = shift_template} = ShiftTemplates.create_shift_template(@valid_attrs)
assert shift_template.description == "some description"
assert shift_template.length_hours == 42
assert shift_template.length_minutes == 42
assert shift_template.location == "some location"
assert shift_template.start_time == ~T[14:00:00]
assert shift_template.time_start == ~T[07:00:00]
assert shift_template.time_end == ~T[15:00:00]
assert shift_template.subject == "some subject"
assert shift_template.timezone == "some timezone"
assert shift_template.time_zone == "some time_zone"
end
test "create_shift_template/1 with invalid data returns error changeset" do
@ -48,12 +47,11 @@ defmodule Shift73k.ShiftTemplatesTest do
shift_template = shift_template_fixture()
assert {:ok, %ShiftTemplate{} = shift_template} = ShiftTemplates.update_shift_template(shift_template, @update_attrs)
assert shift_template.description == "some updated description"
assert shift_template.length_hours == 43
assert shift_template.length_minutes == 43
assert shift_template.location == "some updated location"
assert shift_template.start_time == ~T[15:01:01]
assert shift_template.time_start == ~T[15:00:00]
assert shift_template.time_end == ~T[19:30:00]
assert shift_template.subject == "some updated subject"
assert shift_template.timezone == "some updated timezone"
assert shift_template.time_zone == "some updated time_zone"
end
test "update_shift_template/2 with invalid data returns error changeset" do

View file

@ -7,30 +7,27 @@ defmodule Shift73kWeb.ShiftTemplateLiveTest do
@create_attrs %{
description: "some description",
length_hours: 12,
length_minutes: 42,
location: "some location",
start_time: ~T[14:00:00],
time_start: ~T[08:00:00],
time_end: ~T[16:00:00],
subject: "some subject",
timezone: "some timezone"
time_zone: "some time_zone"
}
@update_attrs %{
description: "some updated description",
length_hours: 12,
length_minutes: 43,
location: "some updated location",
start_time: ~T[15:01:01],
time_start: ~T[15:00:00],
time_end: ~T[19:30:00],
subject: "some updated subject",
timezone: "some updated timezone"
time_zone: "some updated time_zone"
}
@invalid_attrs %{
description: nil,
length_hours: nil,
length_minutes: nil,
location: nil,
start_time: nil,
time_start: nil,
time_end: nil,
subject: nil,
timezone: nil
time_zone: nil
}
defp fixture(:shift_template) do