use strict context boundaries and remove all n+1 queries

This commit is contained in:
2023-03-18 21:06:00 -04:00
parent 0b7146ba32
commit a54cf8b87d
84 changed files with 4345 additions and 3090 deletions

View File

@ -5,6 +5,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup}
alias Ecto.Changeset
alias Phoenix.LiveView.{JS, Socket}
@impl true
@ -18,7 +19,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
) :: {:ok, Socket.t()}
def update(%{ammo_group: ammo_group, current_user: current_user} = assigns, socket) do
changeset =
%ShotGroup{date: NaiveDateTime.utc_now(), count: 1}
%ShotGroup{date: Date.utc_today()}
|> ShotGroup.create_changeset(current_user, ammo_group, %{})
{:ok, socket |> assign(assigns) |> assign(:changeset, changeset)}
@ -32,10 +33,13 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
) do
params = shot_group_params |> process_params(ammo_group)
changeset = %ShotGroup{} |> ShotGroup.create_changeset(current_user, ammo_group, params)
changeset =
%ShotGroup{}
|> ShotGroup.create_changeset(current_user, ammo_group, params)
|> Map.put(:action, :validate)
case changeset |> Changeset.apply_action(:validate) do
{:ok, _data} -> changeset
{:error, changeset} -> changeset
end
{:noreply, socket |> assign(:changeset, changeset)}
end
@ -56,7 +60,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
prompt = dgettext("prompts", "Shots recorded successfully")
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
{:error, %Ecto.Changeset{} = changeset} ->
{:error, %Changeset{} = changeset} ->
socket |> assign(changeset: changeset)
end
@ -65,14 +69,14 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
# calculate count from shots left
defp process_params(params, %AmmoGroup{count: count}) do
new_count =
if params |> Map.get("ammo_left", "0") == "" do
"0"
shot_group_count =
if params |> Map.get("ammo_left", "") == "" do
nil
else
params |> Map.get("ammo_left", "0")
new_count = params |> Map.get("ammo_left") |> String.to_integer()
count - new_count
end
|> String.to_integer()
params |> Map.put("count", count - new_count)
params |> Map.put("count", shot_group_count)
end
end

View File

@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
A component that displays a list of ammo groups
"""
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Repo}
alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoGroup, Containers}
alias Ecto.UUID
alias Phoenix.LiveView.{Rendered, Socket}
@ -54,8 +54,8 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
end
columns = [
%{label: gettext("Purchased on"), key: :purchased_on},
%{label: gettext("Last used on"), key: :used_up_on} | columns
%{label: gettext("Purchased on"), key: :purchased_on, type: Date},
%{label: gettext("Last used on"), key: :used_up_on, type: Date} | columns
]
columns =
@ -94,13 +94,15 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
ammo_type: ammo_type,
columns: columns,
container: container,
original_counts: Ammo.get_original_counts(ammo_groups, current_user),
cprs: Ammo.get_cprs(ammo_groups, current_user),
last_used_dates: ActivityLog.get_last_used_dates(ammo_groups, current_user),
actions: actions,
range: range
}
rows =
ammo_groups
|> Repo.preload([:ammo_type, :container])
|> Enum.map(fn ammo_group ->
ammo_group |> get_row_data_for_ammo_group(extra_data)
end)
@ -124,8 +126,6 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
@spec get_row_data_for_ammo_group(AmmoGroup.t(), additional_data :: map()) :: map()
defp get_row_data_for_ammo_group(ammo_group, %{columns: columns} = additional_data) do
ammo_group = ammo_group |> Repo.preload([:ammo_type, :container])
columns
|> Map.new(fn %{key: key} ->
{key, get_value_for_key(key, ammo_group, additional_data)}
@ -150,30 +150,23 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
defp get_value_for_key(:price_paid, %{price_paid: nil}, _additional_data), do: {"", nil}
defp get_value_for_key(:price_paid, %{price_paid: price_paid}, _additional_data),
do: gettext("$%{amount}", amount: price_paid |> :erlang.float_to_binary(decimals: 2))
defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on}, _additional_data) do
assigns = %{purchased_on: purchased_on}
do: gettext("$%{amount}", amount: display_currency(price_paid))
defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on} = assigns, _additional_data) do
{purchased_on,
~H"""
<.date date={@purchased_on} />
<.date id={"#{@id}-purchased-on"} date={@purchased_on} />
"""}
end
defp get_value_for_key(:used_up_on, ammo_group, _additional_data) do
last_shot_group_date =
case ammo_group |> Ammo.get_last_used_shot_group() do
%{date: last_shot_group_date} -> last_shot_group_date
_no_shot_groups -> nil
end
defp get_value_for_key(:used_up_on, %{id: ammo_group_id}, %{last_used_dates: last_used_dates}) do
last_used_date = last_used_dates |> Map.get(ammo_group_id)
assigns = %{id: ammo_group_id, last_used_date: last_used_date}
assigns = %{last_shot_group_date: last_shot_group_date}
{last_shot_group_date,
{last_used_date,
~H"""
<%= if @last_shot_group_date do %>
<.date date={@last_shot_group_date} />
<%= if @last_used_date do %>
<.date id={"#{@id}-last-used-date"} date={@last_used_date} />
<% else %>
<%= gettext("Never used") %>
<% end %>
@ -189,8 +182,11 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
"""}
end
defp get_value_for_key(:remaining, ammo_group, _additional_data),
do: gettext("%{percentage}%", percentage: ammo_group |> Ammo.get_percentage_remaining())
defp get_value_for_key(:remaining, ammo_group, %{current_user: current_user}),
do:
gettext("%{percentage}%",
percentage: ammo_group |> Ammo.get_percentage_remaining(current_user)
)
defp get_value_for_key(:actions, ammo_group, %{actions: actions}) do
assigns = %{actions: actions, ammo_group: ammo_group}
@ -204,31 +200,40 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
defp get_value_for_key(
:container,
%{container: %{name: container_name}} = ammo_group,
%{container: container}
%{container_id: container_id} = ammo_group,
%{container: container, current_user: current_user}
) do
assigns = %{container: container, ammo_group: ammo_group}
assigns = %{
container:
%{name: container_name} = container_id |> Containers.get_container!(current_user),
container_block: container,
ammo_group: ammo_group
}
{container_name,
~H"""
<%= render_slot(@container, @ammo_group) %>
<%= render_slot(@container_block, {@ammo_group, @container}) %>
"""}
end
defp get_value_for_key(:original_count, ammo_group, _additional_data),
do: ammo_group |> Ammo.get_original_count()
defp get_value_for_key(:original_count, %{id: ammo_group_id}, %{
original_counts: original_counts
}) do
Map.fetch!(original_counts, ammo_group_id)
end
defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data),
do: gettext("No cost information")
defp get_value_for_key(:cpr, ammo_group, _additional_data) do
gettext("$%{amount}",
amount: ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
)
defp get_value_for_key(:cpr, %{id: ammo_group_id}, %{cprs: cprs}) do
gettext("$%{amount}", amount: display_currency(Map.fetch!(cprs, ammo_group_id)))
end
defp get_value_for_key(:count, %{count: count}, _additional_data),
do: if(count == 0, do: gettext("Empty"), else: count)
defp get_value_for_key(key, ammo_group, _additional_data), do: ammo_group |> Map.get(key)
@spec display_currency(float()) :: String.t()
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
end

View File

@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
A component that displays a list of ammo type
"""
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoType}
alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoType}
alias Ecto.UUID
alias Phoenix.LiveView.{Rendered, Socket}
@ -103,13 +103,13 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
[
%{
label: gettext("Used packs"),
key: :used_ammo_count,
type: :used_ammo_count
key: :used_packs_count,
type: :used_packs_count
},
%{
label: gettext("Total ever packs"),
key: :historical_ammo_count,
type: :historical_ammo_count
key: :historical_packs_count,
type: :historical_packs_count
}
]
else
@ -121,7 +121,35 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
%{label: nil, key: "actions", type: :actions, sortable: false}
])
extra_data = %{actions: actions, current_user: current_user}
round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user)
used_counts =
show_used && ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user)
historical_round_counts =
show_used && ammo_types |> Ammo.get_historical_count_for_ammo_types(current_user)
packs_count = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user)
historical_packs_count =
show_used && ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true)
used_packs_count =
show_used && ammo_types |> Ammo.get_used_ammo_groups_count_for_types(current_user)
average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user)
extra_data = %{
actions: actions,
current_user: current_user,
used_counts: used_counts,
round_counts: round_counts,
historical_round_counts: historical_round_counts,
packs_count: packs_count,
used_packs_count: used_packs_count,
historical_packs_count: historical_packs_count,
average_costs: average_costs
}
rows =
ammo_types
@ -156,28 +184,44 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
defp get_ammo_type_value(:boolean, key, ammo_type, _other_data),
do: ammo_type |> Map.get(key) |> humanize()
defp get_ammo_type_value(:round_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
defp get_ammo_type_value(:round_count, _key, %{id: ammo_type_id}, %{round_counts: round_counts}),
do: Map.get(round_counts, ammo_type_id)
defp get_ammo_type_value(:historical_round_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_historical_count_for_ammo_type(current_user)
defp get_ammo_type_value(
:historical_round_count,
_key,
%{id: ammo_type_id},
%{historical_round_counts: historical_round_counts}
),
do: Map.get(historical_round_counts, ammo_type_id)
defp get_ammo_type_value(:used_round_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_used_count_for_ammo_type(current_user)
defp get_ammo_type_value(:used_round_count, _key, %{id: ammo_type_id}, %{
used_counts: used_counts
}),
do: Map.get(used_counts, ammo_type_id)
defp get_ammo_type_value(:historical_ammo_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true)
defp get_ammo_type_value(
:historical_packs_count,
_key,
%{id: ammo_type_id},
%{historical_packs_count: historical_packs_count}
),
do: Map.get(historical_packs_count, ammo_type_id)
defp get_ammo_type_value(:used_ammo_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_used_ammo_groups_count_for_type(current_user)
defp get_ammo_type_value(:used_packs_count, _key, %{id: ammo_type_id}, %{
used_packs_count: used_packs_count
}),
do: Map.get(used_packs_count, ammo_type_id)
defp get_ammo_type_value(:ammo_count, _key, ammo_type, %{current_user: current_user}),
do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user)
defp get_ammo_type_value(:ammo_count, _key, %{id: ammo_type_id}, %{packs_count: packs_count}),
do: Map.get(packs_count, ammo_type_id)
defp get_ammo_type_value(:avg_price_paid, _key, ammo_type, %{current_user: current_user}) do
case ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user) do
defp get_ammo_type_value(:avg_price_paid, _key, %{id: ammo_type_id}, %{
average_costs: average_costs
}) do
case Map.get(average_costs, ammo_type_id) do
nil -> gettext("No cost information")
count -> gettext("$%{amount}", amount: count |> :erlang.float_to_binary(decimals: 2))
count -> gettext("$%{amount}", amount: display_currency(count))
end
end
@ -202,4 +246,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
defp get_ammo_type_value(nil, _key, _ammo_type, _other_data), do: nil
defp get_ammo_type_value(_other, key, ammo_type, _other_data), do: ammo_type |> Map.get(key)
@spec display_currency(float()) :: String.t()
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
end

View File

@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
A component that displays a list of containers
"""
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Containers, Containers.Container, Repo}
alias Cannery.{Accounts.User, Ammo, Containers.Container}
alias Ecto.UUID
alias Phoenix.LiveView.{Rendered, Socket}
@ -45,11 +45,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
%{label: gettext("Name"), key: :name, type: :string},
%{label: gettext("Description"), key: :desc, type: :string},
%{label: gettext("Location"), key: :location, type: :string},
%{label: gettext("Type"), key: :type, type: :string},
%{label: gettext("Packs"), key: :packs, type: :integer},
%{label: gettext("Rounds"), key: :rounds, type: :string},
%{label: gettext("Tags"), key: :tags, type: :tags},
%{label: nil, key: :actions, sortable: false, type: :actions}
%{label: gettext("Type"), key: :type, type: :string}
]
|> Enum.filter(fn %{key: key, type: type} ->
# remove columns if all values match defaults
@ -64,11 +60,19 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
type in [:tags, :actions] or not (container |> Map.get(key) == default_value)
end)
end)
|> Enum.concat([
%{label: gettext("Packs"), key: :packs, type: :integer},
%{label: gettext("Rounds"), key: :rounds, type: :integer},
%{label: gettext("Tags"), key: :tags, type: :tags},
%{label: nil, key: :actions, sortable: false, type: :actions}
])
extra_data = %{
current_user: current_user,
tag_actions: tag_actions,
actions: actions
actions: actions,
pack_count: Ammo.get_ammo_groups_count_for_containers(containers, current_user),
round_count: Ammo.get_round_count_for_containers(containers, current_user)
}
rows =
@ -100,8 +104,6 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
@spec get_row_data_for_container(Container.t(), columns :: [map()], extra_data :: map) :: map()
defp get_row_data_for_container(container, columns, extra_data) do
container = container |> Repo.preload([:ammo_groups, :tags])
columns
|> Map.new(fn %{key: key} -> {key, get_value_for_key(key, container, extra_data)} end)
end
@ -120,18 +122,24 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
"""}
end
defp get_value_for_key(:packs, container, _extra_data) do
container |> Containers.get_container_ammo_group_count!()
defp get_value_for_key(:packs, %{id: container_id}, %{pack_count: pack_count}) do
pack_count |> Map.get(container_id, 0)
end
defp get_value_for_key(:rounds, container, _extra_data) do
container |> Containers.get_container_rounds!()
defp get_value_for_key(:rounds, %{id: container_id}, %{round_count: round_count}) do
round_count |> Map.get(container_id, 0)
end
defp get_value_for_key(:tags, container, %{tag_actions: tag_actions}) do
assigns = %{tag_actions: tag_actions, container: container}
{container.tags |> Enum.map(fn %{name: name} -> name end),
tag_names =
container.tags
|> Enum.map(fn %{name: name} -> name end)
|> Enum.sort()
|> Enum.join(" ")
{tag_names,
~H"""
<div class="flex flex-wrap justify-center items-center">
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />

View File

@ -4,9 +4,9 @@ defmodule CanneryWeb.CoreComponents do
"""
use Phoenix.Component
import CanneryWeb.{Gettext, ViewHelpers}
alias Cannery.{Accounts, Ammo, Ammo.AmmoGroup}
alias Cannery.Accounts.{Invite, Invites, User}
alias Cannery.{Containers, Containers.Container, Tags.Tag}
alias Cannery.{Accounts, Accounts.Invite, Accounts.User}
alias Cannery.{Ammo, Ammo.AmmoGroup}
alias Cannery.{Containers, Containers.Container, Containers.Tag}
alias CanneryWeb.{Endpoint, HomeLive}
alias CanneryWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.{JS, Rendered}
@ -70,6 +70,7 @@ defmodule CanneryWeb.CoreComponents do
def toggle_button(assigns)
attr :container, Container, required: true
attr :current_user, User, required: true
slot(:tag_actions)
slot(:inner_block)
@ -86,73 +87,30 @@ defmodule CanneryWeb.CoreComponents do
def simple_tag_card(assigns)
attr :ammo_group, AmmoGroup, required: true
attr :current_user, User, required: true
attr :original_count, :integer, default: nil
attr :cpr, :integer, default: nil
attr :last_used_date, Date, default: nil
attr :show_container, :boolean, default: false
slot(:inner_block)
def ammo_group_card(assigns)
@spec display_currency(float()) :: String.t()
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
attr :user, User, required: true
slot(:inner_block, required: true)
def user_card(assigns)
attr :invite, Invite, required: true
attr :use_count, :integer, default: nil
attr :current_user, User, required: true
slot(:inner_block)
slot(:code_actions)
def invite_card(%{invite: invite, current_user: current_user} = assigns) do
assigns = assigns |> assign(:use_count, Invites.get_use_count(invite, current_user))
~H"""
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out">
<h1 class="title text-xl">
<%= @invite.name %>
</h1>
<%= if @invite.disabled_at |> is_nil() do %>
<h2 class="title text-md">
<%= if @invite.uses_left do %>
<%= gettext(
"Uses Left: %{uses_left_count}",
uses_left_count: @invite.uses_left
) %>
<% else %>
<%= gettext("Uses Left: Unlimited") %>
<% end %>
</h2>
<% else %>
<h2 class="title text-md">
<%= gettext("Invite Disabled") %>
</h2>
<% end %>
<.qr_code
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
filename={@invite.name}
/>
<h2 :if={@use_count != 0} class="title text-md">
<%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
</h2>
<div class="flex flex-row flex-wrap justify-center items-center">
<code
id={"code-#{@invite.id}"}
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
phx-no-format
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
<%= if @code_actions, do: render_slot(@code_actions) %>
</div>
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
def invite_card(assigns)
attr :content, :string, required: true
attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
@ -164,6 +122,7 @@ defmodule CanneryWeb.CoreComponents do
"""
def qr_code(assigns)
attr :id, :string, required: true
attr :date, :any, required: true, doc: "A `Date` struct or nil"
@doc """
@ -172,6 +131,7 @@ defmodule CanneryWeb.CoreComponents do
"""
def date(assigns)
attr :id, :string, required: true
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
@doc """

View File

@ -17,12 +17,9 @@
<%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
</span>
<span
:if={@ammo_group |> Ammo.get_original_count() != @ammo_group.count}
class="rounded-lg title text-lg"
>
<span :if={@original_count != @ammo_group.count} class="rounded-lg title text-lg">
<%= gettext("Original Count:") %>
<%= @ammo_group |> Ammo.get_original_count() %>
<%= @original_count %>
</span>
<span :if={@ammo_group.notes} class="rounded-lg title text-lg">
@ -32,38 +29,35 @@
<span class="rounded-lg title text-lg">
<%= gettext("Purchased on:") %>
<.date date={@ammo_group.purchased_on} />
<.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} />
</span>
<span :if={@ammo_group |> Ammo.get_last_used_shot_group()} class="rounded-lg title text-lg">
<span :if={@last_used_date} class="rounded-lg title text-lg">
<%= gettext("Last used on:") %>
<.date date={@ammo_group |> Ammo.get_last_used_shot_group() |> Map.get(:date)} />
<.date id={"#{@ammo_group.id}-last-used-on"} date={@last_used_date} />
</span>
<%= if @ammo_group.price_paid do %>
<span class="rounded-lg title text-lg">
<%= gettext("Price paid:") %>
<%= gettext("$%{amount}",
amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
) %>
</span>
<span :if={@ammo_group.price_paid} class="rounded-lg title text-lg">
<%= gettext("Price paid:") %>
<%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %>
</span>
<span class="rounded-lg title text-lg">
<%= gettext("CPR:") %>
<%= gettext("$%{amount}",
amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
) %>
</span>
<% end %>
<span :if={@cpr} class="rounded-lg title text-lg">
<%= gettext("CPR:") %>
<%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
</span>
<span :if={@show_container and @ammo_group.container} class="rounded-lg title text-lg">
<span
:if={@show_container && Containers.get_container!(@ammo_group.container_id, @current_user)}
class="rounded-lg title text-lg"
>
<%= gettext("Container:") %>
<.link
navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container)}
navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container_id)}
class="link"
>
<%= @ammo_group.container.name %>
<%= Containers.get_container!(@ammo_group.container_id, @current_user).name %>
</.link>
</span>
</div>

View File

@ -1,17 +1,17 @@
<div
id={"container-#{@container.id}"}
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
flex flex-col justify-center items-center space-y-4
flex flex-col justify-around items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
<div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
<h1 class="px-4 py-2 rounded-lg title text-xl">
<%= @container.name %>
</h1>
</.link>
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
<h1 class="px-4 py-2 rounded-lg title text-xl">
<%= @container.name %>
</h1>
</.link>
<div class="flex flex-col justify-center items-center space-y-2">
<span :if={@container.desc} class="rounded-lg title text-lg">
<%= gettext("Description:") %>
<%= @container.desc %>
@ -27,20 +27,23 @@
<%= @container.location %>
</span>
<%= unless @container.ammo_groups |> Enum.empty?() do %>
<%= if @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) != 0 do %>
<span class="rounded-lg title text-lg">
<%= gettext("Packs:") %>
<%= @container |> Containers.get_container_ammo_group_count!() %>
<%= @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) %>
</span>
<span class="rounded-lg title text-lg">
<%= gettext("Rounds:") %>
<%= @container |> Containers.get_container_rounds!() %>
<%= @container |> Ammo.get_round_count_for_container!(@current_user) %>
</span>
<% end %>
<div class="flex flex-wrap justify-center items-center">
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
<div
:if={@tag_actions || @container.tags != []}
class="flex flex-wrap justify-center items-center"
>
<.simple_tag_card :for={tag <- @container.tags} tag={tag} />
<%= if @tag_actions, do: render_slot(@tag_actions) %>
</div>

View File

@ -1,12 +1,7 @@
<time
:if={@date}
datetime={@date |> Date.to_iso8601(:extended)}
x-data={~s<{
<time :if={@date} id={@id} datetime={@date |> Date.to_iso8601(:extended)} x-data={~s<{
date:
Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'})
.format(new Date("#{Date.to_iso8601(@date, :extended)}"))
}>}
x-text="date"
>
}>} x-text="date">
<%= @date |> Date.to_iso8601(:extended) %>
</time>

View File

@ -1,12 +1,7 @@
<time
:if={@datetime}
datetime={cast_datetime(@datetime)}
x-data={~s/{
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} x-data={~s/{
datetime:
Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
.format(new Date("#{cast_datetime(@datetime)}"))
}/}
x-text="datetime"
>
}/} x-text="datetime">
<%= cast_datetime(@datetime) %>
</time>

View File

@ -0,0 +1,46 @@
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out">
<h1 class="title text-xl">
<%= @invite.name %>
</h1>
<%= if @invite.disabled_at |> is_nil() do %>
<h2 class="title text-md">
<%= if @invite.uses_left do %>
<%= gettext(
"Uses Left: %{uses_left_count}",
uses_left_count: @invite.uses_left
) %>
<% else %>
<%= gettext("Uses Left: Unlimited") %>
<% end %>
</h2>
<% else %>
<h2 class="title text-md">
<%= gettext("Invite Disabled") %>
</h2>
<% end %>
<.qr_code
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
filename={@invite.name}
/>
<h2 :if={@use_count && @use_count != 0} class="title text-md">
<%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
</h2>
<div class="flex flex-row flex-wrap justify-center items-center">
<code
id={"code-#{@invite.id}"}
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
phx-no-format
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
<%= if @code_actions, do: render_slot(@code_actions) %>
</div>
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
</div>
</div>

View File

@ -15,7 +15,7 @@
"User was confirmed at%{confirmed_datetime}",
confirmed_datetime: ""
) %>
<.datetime datetime={@user.confirmed_at} />
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
<% else %>
<%= gettext("Email unconfirmed") %>
<% end %>
@ -26,7 +26,7 @@
"User registered on%{registered_datetime}",
registered_datetime: ""
) %>
<.datetime datetime={@user.inserted_at} />
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
</p>
</h3>

View File

@ -6,6 +6,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Containers, Containers.Container}
alias CanneryWeb.Endpoint
alias Ecto.Changeset
alias Phoenix.LiveView.Socket
@impl true
@ -51,10 +52,9 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|> case do
{:ok, _ammo_group} ->
prompt = dgettext("prompts", "Ammo moved to %{name} successfully", name: container_name)
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
{:error, %Ecto.Changeset{} = changeset} ->
{:error, %Changeset{} = changeset} ->
socket |> assign(changeset: changeset)
end
@ -64,10 +64,10 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
@impl true
def render(%{containers: containers} = assigns) do
columns = [
%{label: gettext("Container"), key: "name"},
%{label: gettext("Type"), key: "type"},
%{label: gettext("Location"), key: "location"},
%{label: nil, key: "actions", sortable: false}
%{label: gettext("Container"), key: :name},
%{label: gettext("Type"), key: :type},
%{label: gettext("Location"), key: :location},
%{label: nil, key: :actions, sortable: false}
]
rows = containers |> get_rows_for_containers(assigns, columns)
@ -110,8 +110,8 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
end)
end
@spec get_row_value_by_key(String.t(), Container.t(), map()) :: any()
defp get_row_value_by_key("actions", container, assigns) do
@spec get_row_value_by_key(atom(), Container.t(), map()) :: any()
defp get_row_value_by_key(:actions, container, assigns) do
assigns = assigns |> Map.put(:container, container)
~H"""
@ -129,6 +129,5 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
"""
end
defp get_row_value_by_key(key, container, _assigns),
do: container |> Map.get(key |> String.to_existing_atom())
defp get_row_value_by_key(key, container, _assigns), do: container |> Map.get(key)
end

View File

@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
A component that displays a list of shot groups
"""
use CanneryWeb, :live_component
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo}
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo}
alias Ecto.UUID
alias Phoenix.LiveView.{Rendered, Socket}
@ -41,11 +41,16 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
%{label: gettext("Ammo"), key: :name},
%{label: gettext("Rounds shot"), key: :count},
%{label: gettext("Notes"), key: :notes},
%{label: gettext("Date"), key: :date},
%{label: gettext("Date"), key: :date, type: Date},
%{label: nil, key: :actions, sortable: false}
]
extra_data = %{current_user: current_user, actions: actions}
ammo_groups =
shot_groups
|> Enum.map(fn %{ammo_group_id: ammo_group_id} -> ammo_group_id end)
|> Ammo.get_ammo_groups(current_user)
extra_data = %{current_user: current_user, actions: actions, ammo_groups: ammo_groups}
rows =
shot_groups
@ -79,34 +84,28 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
@spec get_row_data_for_shot_group(ShotGroup.t(), columns :: [map()], extra_data :: map()) ::
map()
defp get_row_data_for_shot_group(shot_group, columns, extra_data) do
shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type)
columns
|> Map.new(fn %{key: key} ->
{key, get_row_value(key, shot_group, extra_data)}
end)
end
defp get_row_value(
:name,
%{ammo_group: %{ammo_type: %{name: ammo_type_name} = ammo_group}},
_extra_data
) do
assigns = %{ammo_group: ammo_group, ammo_type_name: ammo_type_name}
defp get_row_value(:name, %{ammo_group_id: ammo_group_id}, %{ammo_groups: ammo_groups}) do
assigns = %{ammo_group: ammo_group = Map.fetch!(ammo_groups, ammo_group_id)}
name_block = ~H"""
<.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="link">
<%= @ammo_type_name %>
</.link>
"""
{ammo_type_name, name_block}
{ammo_group.ammo_type.name,
~H"""
<.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="link">
<%= @ammo_group.ammo_type.name %>
</.link>
"""}
end
defp get_row_value(:date, %{date: _date} = assigns, _extra_data) do
~H"""
<.date date={@date} />
"""
defp get_row_value(:date, %{date: date} = assigns, _extra_data) do
{date,
~H"""
<.date id={"#{@id}-date"} date={@date} />
"""}
end
defp get_row_value(:actions, shot_group, %{actions: actions}) do

View File

@ -33,7 +33,8 @@ defmodule CanneryWeb.Components.TableComponent do
optional(:class) => String.t(),
optional(:row_class) => String.t(),
optional(:alternate_row_class) => String.t(),
optional(:sortable) => false
optional(:sortable) => false,
optional(:type) => module()
}),
required(:rows) =>
list(%{
@ -60,7 +61,8 @@ defmodule CanneryWeb.Components.TableComponent do
:asc
end
rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode)
type = columns |> Enum.find(%{}, fn %{key: key} -> key == initial_key end) |> Map.get(:type)
rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode, type)
socket =
socket
@ -68,6 +70,7 @@ defmodule CanneryWeb.Components.TableComponent do
|> assign(
columns: columns,
rows: rows,
key: initial_key,
last_sort_key: initial_key,
sort_mode: initial_sort_mode
)
@ -81,7 +84,14 @@ defmodule CanneryWeb.Components.TableComponent do
def handle_event(
"sort_by",
%{"sort-key" => key},
%{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
%{
assigns: %{
columns: columns,
rows: rows,
last_sort_key: last_sort_key,
sort_mode: sort_mode
}
} = socket
) do
key = key |> String.to_existing_atom()
@ -92,11 +102,28 @@ defmodule CanneryWeb.Components.TableComponent do
{_new_sort_key, _last_sort_mode} -> :asc
end
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
type =
columns |> Enum.find(%{}, fn %{key: column_key} -> column_key == key end) |> Map.get(:type)
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode, type)
{:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
end
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
when type in [Date, DateTime] do
rows
|> Enum.sort_by(
fn row ->
case row |> Map.get(key) do
{custom_sort_key, _value} -> custom_sort_key
value -> value
end
end,
{sort_mode, type}
)
end
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, _type) do
rows
|> Enum.sort_by(
fn row ->