From a54cf8b87dcf0f00b9bd79c18391a33cc2865cdd Mon Sep 17 00:00:00 2001
From: shibao
Date: Sat, 18 Mar 2023 21:06:00 -0400
Subject: [PATCH] use strict context boundaries and remove all n+1 queries
---
CHANGELOG.md | 2 +
lib/cannery/accounts.ex | 14 +-
lib/cannery/accounts/invites.ex | 20 +-
lib/cannery/activity_log.ex | 141 +++-
lib/cannery/activity_log/shot_group.ex | 61 +-
lib/cannery/ammo.ex | 738 +++++++++++++-----
lib/cannery/ammo/ammo_group.ex | 19 +-
lib/cannery/ammo/ammo_type.ex | 9 +-
lib/cannery/containers.ex | 224 +++++-
lib/cannery/containers/container.ex | 12 +-
lib/cannery/containers/container_tag.ex | 8 +-
lib/cannery/{tags => containers}/tag.ex | 11 +-
lib/cannery/tags.ex | 149 ----
.../components/add_shot_group_component.ex | 26 +-
.../components/ammo_group_table_component.ex | 73 +-
.../components/ammo_type_table_component.ex | 89 ++-
.../components/container_table_component.ex | 36 +-
lib/cannery_web/components/core_components.ex | 70 +-
.../core_components/ammo_group_card.html.heex | 44 +-
.../core_components/container_card.html.heex | 27 +-
.../components/core_components/date.html.heex | 9 +-
.../core_components/datetime.html.heex | 9 +-
.../core_components/invite_card.html.heex | 46 ++
.../core_components/user_card.html.heex | 4 +-
.../components/move_ammo_group_component.ex | 19 +-
.../components/shot_group_table_component.ex | 43 +-
lib/cannery_web/components/table_component.ex | 37 +-
.../controllers/export_controller.ex | 52 +-
.../user_registration_controller.ex | 3 +-
.../live/ammo_group_live/form_component.ex | 73 +-
.../live/ammo_group_live/index.html.heex | 33 +-
lib/cannery_web/live/ammo_group_live/show.ex | 37 +-
.../live/ammo_group_live/show.html.heex | 24 +-
.../live/ammo_type_live/form_component.ex | 15 +-
lib/cannery_web/live/ammo_type_live/index.ex | 6 +-
lib/cannery_web/live/ammo_type_live/show.ex | 24 +-
.../live/ammo_type_live/show.html.heex | 30 +-
.../container_live/edit_tags_component.ex | 5 +-
.../live/container_live/form_component.ex | 12 +-
lib/cannery_web/live/container_live/index.ex | 17 +-
.../live/container_live/index.html.heex | 59 +-
lib/cannery_web/live/container_live/show.ex | 28 +-
.../live/container_live/show.html.heex | 86 +-
lib/cannery_web/live/home_live.ex | 3 +-
lib/cannery_web/live/home_live.html.heex | 12 +-
lib/cannery_web/live/invite_live/index.ex | 12 +-
.../live/invite_live/index.html.heex | 7 +-
.../live/range_live/form_component.ex | 62 +-
lib/cannery_web/live/range_live/index.ex | 14 +-
.../live/range_live/index.html.heex | 62 +-
.../live/tag_live/form_component.ex | 9 +-
lib/cannery_web/live/tag_live/index.ex | 12 +-
priv/gettext/actions.pot | 116 +--
priv/gettext/de/LC_MESSAGES/actions.po | 116 +--
priv/gettext/de/LC_MESSAGES/default.po | 331 ++++----
priv/gettext/de/LC_MESSAGES/errors.po | 71 +-
priv/gettext/de/LC_MESSAGES/prompts.po | 90 +--
priv/gettext/default.pot | 331 ++++----
priv/gettext/en/LC_MESSAGES/actions.po | 116 +--
priv/gettext/en/LC_MESSAGES/default.po | 331 ++++----
priv/gettext/en/LC_MESSAGES/errors.po | 73 +-
priv/gettext/en/LC_MESSAGES/prompts.po | 90 +--
priv/gettext/errors.pot | 71 +-
priv/gettext/es/LC_MESSAGES/actions.po | 116 +--
priv/gettext/es/LC_MESSAGES/default.po | 331 ++++----
priv/gettext/es/LC_MESSAGES/errors.po | 71 +-
priv/gettext/es/LC_MESSAGES/prompts.po | 90 +--
priv/gettext/fr/LC_MESSAGES/actions.po | 116 +--
priv/gettext/fr/LC_MESSAGES/default.po | 331 ++++----
priv/gettext/fr/LC_MESSAGES/errors.po | 71 +-
priv/gettext/fr/LC_MESSAGES/prompts.po | 90 +--
priv/gettext/ga/LC_MESSAGES/actions.po | 116 +--
priv/gettext/ga/LC_MESSAGES/default.po | 331 ++++----
priv/gettext/ga/LC_MESSAGES/errors.po | 73 +-
priv/gettext/ga/LC_MESSAGES/prompts.po | 90 +--
priv/gettext/prompts.pot | 90 +--
test/cannery/accounts/invites_test.exs | 36 +-
test/cannery/activity_log_test.exs | 153 ++++
test/cannery/ammo_test.exs | 701 +++++++++++++----
test/cannery/containers_test.exs | 111 ++-
test/cannery/tags_test.exs | 90 ---
.../controllers/export_controller_test.exs | 23 +-
.../cannery_web/live/ammo_group_live_test.exs | 25 +-
test/support/fixtures.ex | 7 +-
84 files changed, 4345 insertions(+), 3090 deletions(-)
rename lib/cannery/{tags => containers}/tag.ex (89%)
delete mode 100644 lib/cannery/tags.ex
create mode 100644 lib/cannery_web/components/core_components/invite_card.html.heex
delete mode 100644 test/cannery/tags_test.exs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a100790..52a6d629 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
- Code quality improvements
- Fix dead link of example bullet abbreviations
- Fix inaccurate error message when updating shot records
+- Fix tables not sorting dates correctly
+- Fix container table not displaying all fields
# v0.8.3
- Improve some styles
diff --git a/lib/cannery/accounts.ex b/lib/cannery/accounts.ex
index 0adb6d2a..6cf6efc6 100644
--- a/lib/cannery/accounts.ex
+++ b/lib/cannery/accounts.ex
@@ -385,8 +385,18 @@ defmodule Cannery.Accounts do
"""
@spec allow_registration?() :: boolean()
def allow_registration? do
- Application.get_env(:cannery, Cannery.Accounts)[:registration] == "public" or
- list_users_by_role(:admin) |> Enum.empty?()
+ registration_mode() == :public or list_users_by_role(:admin) |> Enum.empty?()
+ end
+
+ @doc """
+ Returns an atom representing the current configured registration mode
+ """
+ @spec registration_mode() :: :public | :invite_only
+ def registration_mode do
+ case Application.get_env(:cannery, Cannery.Accounts)[:registration] do
+ "public" -> :public
+ _other -> :invite_only
+ end
end
@doc """
diff --git a/lib/cannery/accounts/invites.ex b/lib/cannery/accounts/invites.ex
index 7f8e955d..93c0f303 100644
--- a/lib/cannery/accounts/invites.ex
+++ b/lib/cannery/accounts/invites.ex
@@ -100,13 +100,23 @@ defmodule Cannery.Accounts.Invites do
end
end
- @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer()
- def get_use_count(%Invite{id: invite_id}, %User{role: :admin}) do
- Repo.one(
+ @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | nil
+ def get_use_count(%Invite{id: invite_id} = invite, user) do
+ [invite] |> get_use_counts(user) |> Map.get(invite_id)
+ end
+
+ @spec get_use_counts([Invite.t()], User.t()) ::
+ %{optional(Invite.id()) => non_neg_integer()}
+ def get_use_counts(invites, %User{role: :admin}) do
+ invite_ids = invites |> Enum.map(fn %{id: invite_id} -> invite_id end)
+
+ Repo.all(
from u in User,
- where: u.invite_id == ^invite_id,
- select: count(u.id)
+ where: u.invite_id in ^invite_ids,
+ group_by: u.invite_id,
+ select: {u.invite_id, count(u.id)}
)
+ |> Map.new()
end
@spec decrement_invite_changeset(Invite.t()) :: Invite.changeset()
diff --git a/lib/cannery/activity_log.ex b/lib/cannery/activity_log.ex
index 3d9aa35e..d24bbf03 100644
--- a/lib/cannery/activity_log.ex
+++ b/lib/cannery/activity_log.ex
@@ -4,7 +4,8 @@ defmodule Cannery.ActivityLog do
"""
import Ecto.Query, warn: false
- alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
+ alias Cannery.Ammo.{AmmoGroup, AmmoType}
+ alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo}
alias Ecto.Multi
@doc """
@@ -31,8 +32,10 @@ defmodule Cannery.ActivityLog do
Repo.all(
from sg in ShotGroup,
- left_join: ag in assoc(sg, :ammo_group),
- left_join: at in assoc(ag, :ammo_type),
+ left_join: ag in AmmoGroup,
+ on: sg.ammo_group_id == ag.id,
+ left_join: at in AmmoType,
+ on: ag.ammo_type_id == at.id,
where: sg.user_id == ^user_id,
where:
fragment(
@@ -61,6 +64,18 @@ defmodule Cannery.ActivityLog do
)
end
+ @spec list_shot_groups_for_ammo_group(AmmoGroup.t(), User.t()) :: [ShotGroup.t()]
+ def list_shot_groups_for_ammo_group(
+ %AmmoGroup{id: ammo_group_id, user_id: user_id},
+ %User{id: user_id}
+ ) do
+ Repo.all(
+ from sg in ShotGroup,
+ where: sg.ammo_group_id == ^ammo_group_id,
+ where: sg.user_id == ^user_id
+ )
+ end
+
@doc """
Gets a single shot_group.
@@ -107,9 +122,15 @@ defmodule Cannery.ActivityLog do
)
|> Multi.run(
:ammo_group,
- fn repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
- {:ok,
- repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)}
+ fn _repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
+ ammo_group =
+ Repo.one(
+ from ag in AmmoGroup,
+ where: ag.id == ^ammo_group_id,
+ where: ag.user_id == ^user_id
+ )
+
+ {:ok, ammo_group}
end
)
|> Multi.update(
@@ -220,4 +241,112 @@ defmodule Cannery.ActivityLog do
{:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
end
end
+
+ @doc """
+ Returns the number of shot rounds for an ammo group
+ """
+ @spec get_used_count(AmmoGroup.t(), User.t()) :: non_neg_integer()
+ def get_used_count(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
+ [ammo_group]
+ |> get_used_counts(user)
+ |> Map.get(ammo_group_id, 0)
+ end
+
+ @doc """
+ Returns the number of shot rounds for multiple ammo groups
+ """
+ @spec get_used_counts([AmmoGroup.t()], User.t()) ::
+ %{optional(AmmoGroup.id()) => non_neg_integer()}
+ def get_used_counts(ammo_groups, %User{id: user_id}) do
+ ammo_group_ids =
+ ammo_groups
+ |> Enum.map(fn %{id: ammo_group_id} -> ammo_group_id end)
+
+ Repo.all(
+ from sg in ShotGroup,
+ where: sg.ammo_group_id in ^ammo_group_ids,
+ where: sg.user_id == ^user_id,
+ group_by: sg.ammo_group_id,
+ select: {sg.ammo_group_id, sum(sg.count)}
+ )
+ |> Map.new()
+ end
+
+ @doc """
+ Returns the last entered shot group date for an ammo group
+ """
+ @spec get_last_used_date(AmmoGroup.t(), User.t()) :: Date.t() | nil
+ def get_last_used_date(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
+ [ammo_group]
+ |> get_last_used_dates(user)
+ |> Map.get(ammo_group_id)
+ end
+
+ @doc """
+ Returns the last entered shot group date for an ammo group
+ """
+ @spec get_last_used_dates([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => Date.t()}
+ def get_last_used_dates(ammo_groups, %User{id: user_id}) do
+ ammo_group_ids =
+ ammo_groups
+ |> Enum.map(fn %AmmoGroup{id: ammo_group_id, user_id: ^user_id} -> ammo_group_id end)
+
+ Repo.all(
+ from sg in ShotGroup,
+ where: sg.ammo_group_id in ^ammo_group_ids,
+ where: sg.user_id == ^user_id,
+ group_by: sg.ammo_group_id,
+ select: {sg.ammo_group_id, max(sg.date)}
+ )
+ |> Map.new()
+ end
+
+ @doc """
+ Gets the total number of rounds shot for an ammo type
+
+ Raises `Ecto.NoResultsError` if the Ammo type does not exist.
+
+ ## Examples
+
+ iex> get_used_count_for_ammo_type(123, %User{id: 123})
+ 35
+
+ iex> get_used_count_for_ammo_type(456, %User{id: 123})
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
+ def get_used_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
+ [ammo_type]
+ |> get_used_count_for_ammo_types(user)
+ |> Map.get(ammo_type_id, 0)
+ end
+
+ @doc """
+ Gets the total number of rounds shot for multiple ammo types
+
+ ## Examples
+
+ iex> get_used_count_for_ammo_types(123, %User{id: 123})
+ 35
+
+ """
+ @spec get_used_count_for_ammo_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ def get_used_count_for_ammo_types(ammo_types, %User{id: user_id}) do
+ ammo_type_ids =
+ ammo_types
+ |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
+
+ Repo.all(
+ from ag in AmmoGroup,
+ left_join: sg in ShotGroup,
+ on: ag.id == sg.ammo_group_id,
+ where: ag.ammo_type_id in ^ammo_type_ids,
+ where: not (sg.count |> is_nil()),
+ group_by: ag.ammo_type_id,
+ select: {ag.ammo_type_id, sum(sg.count)}
+ )
+ |> Map.new()
+ end
end
diff --git a/lib/cannery/activity_log/shot_group.ex b/lib/cannery/activity_log/shot_group.ex
index f4a66ad5..32d7e2c8 100644
--- a/lib/cannery/activity_log/shot_group.ex
+++ b/lib/cannery/activity_log/shot_group.ex
@@ -6,7 +6,7 @@ defmodule Cannery.ActivityLog.ShotGroup do
use Ecto.Schema
import CanneryWeb.Gettext
import Ecto.Changeset
- alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
+ alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup}
alias Ecto.{Changeset, UUID}
@derive {Jason.Encoder,
@@ -24,25 +24,23 @@ defmodule Cannery.ActivityLog.ShotGroup do
field :date, :date
field :notes, :string
- belongs_to :user, User
- belongs_to :ammo_group, AmmoGroup
+ field :user_id, :binary_id
+ field :ammo_group_id, :binary_id
timestamps()
end
- @type t :: %ShotGroup{
+ @type t :: %__MODULE__{
id: id(),
count: integer,
notes: String.t() | nil,
date: Date.t() | nil,
- ammo_group: AmmoGroup.t() | nil,
ammo_group_id: AmmoGroup.id(),
- user: User.t() | nil,
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_shot_group :: %ShotGroup{}
+ @type new_shot_group :: %__MODULE__{}
@type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_shot_group())
@@ -58,44 +56,47 @@ defmodule Cannery.ActivityLog.ShotGroup do
%User{id: user_id},
%AmmoGroup{id: ammo_group_id, user_id: user_id} = ammo_group,
attrs
- )
- when not (user_id |> is_nil()) and not (ammo_group_id |> is_nil()) do
+ ) do
shot_group
|> change(user_id: user_id)
|> change(ammo_group_id: ammo_group_id)
|> cast(attrs, [:count, :notes, :date])
- |> validate_number(:count, greater_than: 0)
|> validate_create_shot_group_count(ammo_group)
- |> validate_required([:count, :date, :ammo_group_id, :user_id])
+ |> validate_required([:date, :ammo_group_id, :user_id])
end
def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do
shot_group
|> cast(attrs, [:count, :notes, :date])
- |> validate_number(:count, greater_than: 0)
- |> validate_required([:count, :ammo_group_id, :user_id])
+ |> validate_required([:ammo_group_id, :user_id])
|> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo pack"))
end
defp validate_create_shot_group_count(changeset, %AmmoGroup{count: ammo_group_count}) do
- if changeset |> Changeset.get_field(:count) > ammo_group_count do
- error =
- dgettext("errors", "Count must be less than %{count} shots", count: ammo_group_count)
+ case changeset |> Changeset.get_field(:count) do
+ nil ->
+ changeset |> Changeset.add_error(:ammo_left, dgettext("errors", "can't be blank"))
- changeset |> Changeset.add_error(:count, error)
- else
- changeset
+ count when count > ammo_group_count ->
+ changeset
+ |> Changeset.add_error(:ammo_left, dgettext("errors", "Ammo left must be at least 0"))
+
+ count when count <= 0 ->
+ error =
+ dgettext("errors", "Ammo left can be at most %{count} rounds",
+ count: ammo_group_count - 1
+ )
+
+ changeset |> Changeset.add_error(:ammo_left, error)
+
+ _valid_count ->
+ changeset
end
end
@doc false
@spec update_changeset(t() | new_shot_group(), User.t(), attrs :: map()) :: changeset()
- def update_changeset(
- %ShotGroup{user_id: user_id} = shot_group,
- %User{id: user_id} = user,
- attrs
- )
- when not (user_id |> is_nil()) do
+ def update_changeset(%__MODULE__{} = shot_group, user, attrs) do
shot_group
|> cast(attrs, [:count, :notes, :date])
|> validate_number(:count, greater_than: 0)
@@ -105,12 +106,10 @@ defmodule Cannery.ActivityLog.ShotGroup do
defp validate_update_shot_group_count(
changeset,
- %ShotGroup{count: count} = shot_group,
- %User{id: user_id}
- )
- when not (user_id |> is_nil()) do
- %{ammo_group: %AmmoGroup{count: ammo_group_count, user_id: ^user_id}} =
- shot_group |> Repo.preload(:ammo_group)
+ %__MODULE__{ammo_group_id: ammo_group_id, count: count},
+ user
+ ) do
+ %{count: ammo_group_count} = Ammo.get_ammo_group!(ammo_group_id, user)
new_shot_group_count = changeset |> Changeset.get_field(:count)
shot_diff_to_add = new_shot_group_count - count
diff --git a/lib/cannery/ammo.ex b/lib/cannery/ammo.ex
index ece49a80..3760e8c7 100644
--- a/lib/cannery/ammo.ex
+++ b/lib/cannery/ammo.ex
@@ -5,12 +5,15 @@ defmodule Cannery.Ammo do
import CanneryWeb.Gettext
import Ecto.Query, warn: false
- alias Cannery.{Accounts.User, Containers, Containers.Container, Repo}
- alias Cannery.ActivityLog.ShotGroup
+ alias Cannery.{Accounts.User, Containers, Repo}
+ alias Cannery.Containers.{Container, ContainerTag, Tag}
+ alias Cannery.{ActivityLog, ActivityLog.ShotGroup}
alias Cannery.Ammo.{AmmoGroup, AmmoType}
alias Ecto.Changeset
@ammo_group_create_limit 10_000
+ @ammo_group_preloads [:ammo_type]
+ @ammo_type_preloads [:ammo_groups]
@doc """
Returns the list of ammo_types.
@@ -28,8 +31,14 @@ defmodule Cannery.Ammo do
@spec list_ammo_types(search :: nil | String.t(), User.t()) :: [AmmoType.t()]
def list_ammo_types(search \\ nil, user)
- def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "",
- do: Repo.all(from at in AmmoType, where: at.user_id == ^user_id, order_by: at.name)
+ def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "" do
+ Repo.all(
+ from at in AmmoType,
+ where: at.user_id == ^user_id,
+ order_by: at.name,
+ preload: ^@ammo_type_preloads
+ )
+ end
def list_ammo_types(search, %{id: user_id}) when search |> is_binary() do
trimmed_search = String.trim(search)
@@ -39,16 +48,19 @@ defmodule Cannery.Ammo do
where: at.user_id == ^user_id,
where:
fragment(
- "search @@ websearch_to_tsquery('english', ?)",
+ "? @@ websearch_to_tsquery('english', ?)",
+ at.search,
^trimmed_search
),
order_by: {
:desc,
fragment(
- "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)",
+ "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
+ at.search,
^trimmed_search
)
- }
+ },
+ preload: ^@ammo_type_preloads
)
end
@@ -86,114 +98,169 @@ defmodule Cannery.Ammo do
"""
@spec get_ammo_type!(AmmoType.id(), User.t()) :: AmmoType.t()
- def get_ammo_type!(id, %User{id: user_id}),
- do: Repo.one!(from at in AmmoType, where: at.id == ^id and at.user_id == ^user_id)
+ def get_ammo_type!(id, %User{id: user_id}) do
+ Repo.one!(
+ from at in AmmoType,
+ where: at.id == ^id,
+ where: at.user_id == ^user_id,
+ preload: ^@ammo_type_preloads
+ )
+ end
@doc """
- Gets the average cost of a single ammo type
+ Gets the average cost of an ammo type from ammo groups with price information.
## Examples
- iex> get_average_cost_for_ammo_type!(%AmmoType{id: 123}, %User{id: 123})
+ iex> get_average_cost_for_ammo_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
1.50
"""
- @spec get_average_cost_for_ammo_type!(AmmoType.t(), User.t()) :: float() | nil
- def get_average_cost_for_ammo_type!(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id}
- ) do
+ @spec get_average_cost_for_ammo_type(AmmoType.t(), User.t()) :: float() | nil
+ def get_average_cost_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
+ [ammo_type]
+ |> get_average_cost_for_ammo_types(user)
+ |> Map.get(ammo_type_id)
+ end
+
+ @doc """
+ Gets the average cost of ammo types from ammo groups with price information
+ for multiple ammo types.
+
+ ## Examples
+
+ iex> get_average_cost_for_ammo_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ 1.50
+
+ """
+ @spec get_average_cost_for_ammo_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => float()}
+ def get_average_cost_for_ammo_types(ammo_types, %User{id: user_id}) do
+ ammo_type_ids =
+ ammo_types
+ |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
+
sg_total_query =
from sg in ShotGroup,
where: not (sg.count |> is_nil()),
group_by: sg.ammo_group_id,
select: %{ammo_group_id: sg.ammo_group_id, total: sum(sg.count)}
- Repo.one!(
+ Repo.all(
from ag in AmmoGroup,
as: :ammo_group,
left_join: sg_query in subquery(sg_total_query),
on: ag.id == sg_query.ammo_group_id,
- where: ag.ammo_type_id == ^ammo_type_id,
+ where: ag.ammo_type_id in ^ammo_type_ids,
+ group_by: ag.ammo_type_id,
where: not (ag.price_paid |> is_nil()),
- select: sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0))
+ select:
+ {ag.ammo_type_id, sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0))}
)
+ |> Map.new()
end
@doc """
Gets the total number of rounds for an ammo type
- Raises `Ecto.NoResultsError` if the Ammo type does not exist.
-
## Examples
- iex> get_round_count_for_ammo_type(123, %User{id: 123})
+ iex> get_round_count_for_ammo_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
35
- iex> get_round_count_for_ammo_type(456, %User{id: 123})
- ** (Ecto.NoResultsError)
-
"""
@spec get_round_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
- def get_round_count_for_ammo_type(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id}
- ) do
- Repo.one!(
- from ag in AmmoGroup,
- where: ag.ammo_type_id == ^ammo_type_id,
- select: sum(ag.count)
- ) || 0
+ def get_round_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
+ [ammo_type]
+ |> get_round_count_for_ammo_types(user)
+ |> Map.get(ammo_type_id, 0)
end
@doc """
- Gets the total number of rounds shot for an ammo type
-
- Raises `Ecto.NoResultsError` if the Ammo type does not exist.
+ Gets the total number of rounds for multiple ammo types
## Examples
- iex> get_used_count_for_ammo_type(123, %User{id: 123})
- 35
-
- iex> get_used_count_for_ammo_type(456, %User{id: 123})
- ** (Ecto.NoResultsError)
+ iex> get_round_count_for_ammo_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 35}
"""
- @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
- def get_used_count_for_ammo_type(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id}
- ) do
- Repo.one!(
+ @spec get_round_count_for_ammo_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ def get_round_count_for_ammo_types(ammo_types, %User{id: user_id}) do
+ ammo_type_ids =
+ ammo_types
+ |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
+
+ Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
- where: ag.ammo_type_id == ^ammo_type_id,
- select: sum(sg.count)
- ) || 0
+ where: ag.ammo_type_id in ^ammo_type_ids,
+ where: ag.user_id == ^user_id,
+ group_by: ag.ammo_type_id,
+ select: {ag.ammo_type_id, sum(ag.count)}
+ )
+ |> Map.new()
end
@doc """
Gets the total number of ammo ever bought for an ammo type
- Raises `Ecto.NoResultsError` if the Ammo type does not exist.
-
## Examples
- iex> get_historical_count_for_ammo_type(123, %User{id: 123})
- %AmmoType{}
-
- iex> get_historical_count_for_ammo_type(456, %User{id: 123})
- ** (Ecto.NoResultsError)
+ iex> get_historical_count_for_ammo_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 5
"""
@spec get_historical_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
- def get_historical_count_for_ammo_type(
- %AmmoType{user_id: user_id} = ammo_type,
- %User{id: user_id} = user
- ) do
- get_round_count_for_ammo_type(ammo_type, user) +
- get_used_count_for_ammo_type(ammo_type, user)
+ def get_historical_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
+ [ammo_type]
+ |> get_historical_count_for_ammo_types(user)
+ |> Map.get(ammo_type_id, 0)
+ end
+
+ @doc """
+ Gets the total number of ammo ever bought for multiple ammo types
+
+ ## Examples
+
+ iex> get_historical_count_for_ammo_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 5}
+
+ """
+ @spec get_historical_count_for_ammo_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ def get_historical_count_for_ammo_types(ammo_types, %User{id: user_id} = user) do
+ used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(user)
+ round_counts = ammo_types |> get_round_count_for_ammo_types(user)
+
+ ammo_types
+ |> Enum.filter(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} ->
+ Map.has_key?(used_counts, ammo_type_id) or Map.has_key?(round_counts, ammo_type_id)
+ end)
+ |> Map.new(fn %{id: ammo_type_id} ->
+ historical_count =
+ Map.get(used_counts, ammo_type_id, 0) + Map.get(round_counts, ammo_type_id, 0)
+
+ {ammo_type_id, historical_count}
+ end)
end
@doc """
@@ -210,8 +277,21 @@ defmodule Cannery.Ammo do
"""
@spec create_ammo_type(attrs :: map(), User.t()) ::
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
- def create_ammo_type(attrs \\ %{}, %User{} = user),
- do: %AmmoType{} |> AmmoType.create_changeset(user, attrs) |> Repo.insert()
+ def create_ammo_type(attrs \\ %{}, %User{} = user) do
+ %AmmoType{}
+ |> AmmoType.create_changeset(user, attrs)
+ |> Repo.insert()
+ |> case do
+ {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
+
+ @spec preload_ammo_type(AmmoType.t()) :: AmmoType.t()
+ @spec preload_ammo_type([AmmoType.t()]) :: [AmmoType.t()]
+ defp preload_ammo_type(ammo_type_or_ammo_types) do
+ ammo_type_or_ammo_types |> Repo.preload(@ammo_type_preloads)
+ end
@doc """
Updates a ammo_type.
@@ -227,8 +307,15 @@ defmodule Cannery.Ammo do
"""
@spec update_ammo_type(AmmoType.t(), attrs :: map(), User.t()) ::
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
- def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}),
- do: ammo_type |> AmmoType.update_changeset(attrs) |> Repo.update()
+ def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}) do
+ ammo_type
+ |> AmmoType.update_changeset(attrs)
+ |> Repo.update()
+ |> case do
+ {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
@doc """
Deletes a ammo_type.
@@ -244,30 +331,48 @@ defmodule Cannery.Ammo do
"""
@spec delete_ammo_type(AmmoType.t(), User.t()) ::
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
- def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
- do: ammo_type |> Repo.delete()
+ def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}) do
+ ammo_type
+ |> Repo.delete()
+ |> case do
+ {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
@doc """
Deletes a ammo_type.
## Examples
- iex> delete_ammo_type(ammo_type, %User{id: 123})
+ iex> delete_ammo_type!(ammo_type, %User{id: 123})
%AmmoType{}
"""
@spec delete_ammo_type!(AmmoType.t(), User.t()) :: AmmoType.t()
- def delete_ammo_type!(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
- do: ammo_type |> Repo.delete!()
+ def delete_ammo_type!(ammo_type, user) do
+ {:ok, ammo_type} = delete_ammo_type(ammo_type, user)
+ ammo_type
+ end
@doc """
Returns the list of ammo_groups for a user and type.
## Examples
- iex> list_ammo_groups_for_type(%AmmoType{id: 123}, %User{id: 123})
+ iex> list_ammo_groups_for_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
[%AmmoGroup{}, ...]
+ iex> list_ammo_groups_for_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456},
+ ...> true
+ ...> )
+ [%AmmoGroup{}, %AmmoGroup{}, ...]
+
"""
@spec list_ammo_groups_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
@@ -281,11 +386,9 @@ defmodule Cannery.Ammo do
) do
Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
where: ag.ammo_type_id == ^ammo_type_id,
where: ag.user_id == ^user_id,
- preload: [shot_groups: sg],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
end
@@ -296,12 +399,10 @@ defmodule Cannery.Ammo do
) do
Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
where: ag.ammo_type_id == ^ammo_type_id,
where: ag.user_id == ^user_id,
where: not (ag.count == 0),
- preload: [shot_groups: sg],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
end
@@ -310,9 +411,19 @@ defmodule Cannery.Ammo do
## Examples
- iex> list_ammo_groups_for_container(%AmmoType{id: 123}, %User{id: 123})
+ iex> list_ammo_groups_for_container(
+ ...> %Container{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
[%AmmoGroup{}, ...]
+ iex> list_ammo_groups_for_container(
+ ...> %Container{id: 123, user_id: 456},
+ ...> %User{id: 456},
+ ...> true
+ ...> )
+ [%AmmoGroup{}, %AmmoGroup{}, ...]
+
"""
@spec list_ammo_groups_for_container(Container.t(), User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups_for_container(Container.t(), User.t(), include_empty :: boolean()) ::
@@ -326,11 +437,9 @@ defmodule Cannery.Ammo do
) do
Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
where: ag.container_id == ^container_id,
where: ag.user_id == ^user_id,
- preload: [shot_groups: sg],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
end
@@ -341,12 +450,10 @@ defmodule Cannery.Ammo do
) do
Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
where: ag.container_id == ^container_id,
where: ag.user_id == ^user_id,
where: not (ag.count == 0),
- preload: [shot_groups: sg],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
end
@@ -355,45 +462,76 @@ defmodule Cannery.Ammo do
## Examples
- iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123})
+ iex> get_ammo_groups_count_for_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
3
- iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123}, true)
+ iex> get_ammo_groups_count_for_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456},
+ ...> true
+ ...> )
5
"""
- @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
+ @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
@spec get_ammo_groups_count_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
- [AmmoGroup.t()]
- def get_ammo_groups_count_for_type(ammo_type, user, include_empty \\ false)
-
+ non_neg_integer()
def get_ammo_groups_count_for_type(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id},
- true = _include_empty
+ %AmmoType{id: ammo_type_id} = ammo_type,
+ user,
+ include_empty \\ false
) do
- Repo.one!(
- from ag in AmmoGroup,
- where: ag.user_id == ^user_id,
- where: ag.ammo_type_id == ^ammo_type_id,
- distinct: true,
- select: count(ag.id)
- ) || 0
+ [ammo_type]
+ |> get_ammo_groups_count_for_types(user, include_empty)
+ |> Map.get(ammo_type_id, 0)
end
- def get_ammo_groups_count_for_type(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id},
- false = _include_empty
- ) do
- Repo.one!(
- from ag in AmmoGroup,
- where: ag.user_id == ^user_id,
- where: ag.ammo_type_id == ^ammo_type_id,
- where: not (ag.count == 0),
- distinct: true,
- select: count(ag.id)
- ) || 0
+ @doc """
+ Returns the count of ammo_groups for multiple ammo types.
+
+ ## Examples
+
+ iex> get_ammo_groups_count_for_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ 3
+
+ iex> get_ammo_groups_count_for_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456},
+ ...> true
+ ...> )
+ 5
+
+ """
+ @spec get_ammo_groups_count_for_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ @spec get_ammo_groups_count_for_types([AmmoType.t()], User.t(), include_empty :: boolean()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ def get_ammo_groups_count_for_types(ammo_types, %User{id: user_id}, include_empty \\ false) do
+ ammo_type_ids =
+ ammo_types
+ |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
+
+ from(ag in AmmoGroup,
+ where: ag.user_id == ^user_id,
+ where: ag.ammo_type_id in ^ammo_type_ids,
+ group_by: ag.ammo_type_id,
+ select: {ag.ammo_type_id, count(ag.id)}
+ )
+ |> maybe_include_empty(include_empty)
+ |> Repo.all()
+ |> Map.new()
+ end
+
+ defp maybe_include_empty(query, true), do: query
+
+ defp maybe_include_empty(query, _false) do
+ query |> where([ag], not (ag.count == 0))
end
@doc """
@@ -401,23 +539,147 @@ defmodule Cannery.Ammo do
## Examples
- iex> get_used_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123})
+ iex> get_used_ammo_groups_count_for_type(
+ ...> %AmmoType{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
3
"""
- @spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
- def get_used_ammo_groups_count_for_type(
- %AmmoType{id: ammo_type_id, user_id: user_id},
- %User{id: user_id}
- ) do
- Repo.one!(
+ @spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
+ def get_used_ammo_groups_count_for_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
+ [ammo_type]
+ |> get_used_ammo_groups_count_for_types(user)
+ |> Map.get(ammo_type_id, 0)
+ end
+
+ @doc """
+ Returns the count of used ammo_groups for multiple ammo types.
+
+ ## Examples
+
+ iex> get_used_ammo_groups_count_for_types(
+ ...> [%AmmoType{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 3}
+
+ """
+ @spec get_used_ammo_groups_count_for_types([AmmoType.t()], User.t()) ::
+ %{optional(AmmoType.id()) => non_neg_integer()}
+ def get_used_ammo_groups_count_for_types(ammo_types, %User{id: user_id}) do
+ ammo_type_ids =
+ ammo_types
+ |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
+
+ Repo.all(
from ag in AmmoGroup,
where: ag.user_id == ^user_id,
- where: ag.ammo_type_id == ^ammo_type_id,
+ where: ag.ammo_type_id in ^ammo_type_ids,
where: ag.count == 0,
- distinct: true,
- select: count(ag.id)
- ) || 0
+ group_by: ag.ammo_type_id,
+ select: {ag.ammo_type_id, count(ag.id)}
+ )
+ |> Map.new()
+ end
+
+ @doc """
+ Returns number of ammo packs in a container.
+
+ ## Examples
+
+ iex> get_ammo_groups_count_for_container(
+ ...> %Container{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 3
+
+ """
+ @spec get_ammo_groups_count_for_container!(Container.t(), User.t()) :: non_neg_integer()
+ def get_ammo_groups_count_for_container!(
+ %Container{id: container_id} = container,
+ %User{} = user
+ ) do
+ [container]
+ |> get_ammo_groups_count_for_containers(user)
+ |> Map.get(container_id, 0)
+ end
+
+ @doc """
+ Returns number of ammo packs in multiple containers.
+
+ ## Examples
+
+ iex> get_ammo_groups_count_for_containers(
+ ...> [%Container{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 3}
+
+ """
+ @spec get_ammo_groups_count_for_containers([Container.t()], User.t()) :: %{
+ Container.id() => non_neg_integer()
+ }
+ def get_ammo_groups_count_for_containers(containers, %User{id: user_id}) do
+ container_ids =
+ containers
+ |> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end)
+
+ Repo.all(
+ from ag in AmmoGroup,
+ where: ag.container_id in ^container_ids,
+ where: ag.count != 0,
+ group_by: ag.container_id,
+ select: {ag.container_id, count(ag.id)}
+ )
+ |> Map.new()
+ end
+
+ @doc """
+ Returns number of rounds in a container.
+
+ ## Examples
+
+ iex> get_round_count_for_container(
+ ...> %Container{id: 123, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 5
+
+ """
+ @spec get_round_count_for_container!(Container.t(), User.t()) :: non_neg_integer()
+ def get_round_count_for_container!(%Container{id: container_id} = container, user) do
+ [container]
+ |> get_round_count_for_containers(user)
+ |> Map.get(container_id, 0)
+ end
+
+ @doc """
+ Returns number of ammo packs in multiple containers.
+
+ ## Examples
+
+ iex> get_round_count_for_containers(
+ ...> [%Container{id: 123, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 5}
+
+ """
+ @spec get_round_count_for_containers([Container.t()], User.t()) ::
+ %{Container.id() => non_neg_integer()}
+ def get_round_count_for_containers(containers, %User{id: user_id}) do
+ container_ids =
+ containers
+ |> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end)
+
+ Repo.all(
+ from ag in AmmoGroup,
+ where: ag.container_id in ^container_ids,
+ group_by: ag.container_id,
+ select: {ag.container_id, sum(ag.count)}
+ )
+ |> Map.new()
end
@doc """
@@ -440,17 +702,20 @@ defmodule Cannery.Ammo do
from(
ag in AmmoGroup,
as: :ag,
- left_join: sg in assoc(ag, :shot_groups),
- as: :sg,
join: at in assoc(ag, :ammo_type),
as: :at,
- join: c in assoc(ag, :container),
+ join: c in Container,
+ on: ag.container_id == c.id,
+ on: ag.user_id == c.user_id,
as: :c,
- left_join: t in assoc(c, :tags),
+ left_join: ct in ContainerTag,
+ on: c.id == ct.container_id,
+ left_join: t in Tag,
+ on: ct.tag_id == t.id,
+ on: c.user_id == t.user_id,
as: :t,
where: ag.user_id == ^user_id,
- preload: [shot_groups: sg, ammo_type: at, container: {c, tags: t}],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
|> list_ammo_groups_include_empty(include_empty)
|> list_ammo_groups_search(search)
@@ -517,18 +782,16 @@ defmodule Cannery.Ammo do
def list_staged_ammo_groups(%User{id: user_id}) do
Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
where: ag.user_id == ^user_id,
where: ag.staged == true,
- preload: [shot_groups: sg],
- order_by: ag.id
+ preload: ^@ammo_group_preloads
)
end
@doc """
Gets a single ammo_group.
- Raises `Ecto.NoResultsError` if the Ammo group does not exist.
+ Raises `KeyError` if the Ammo group does not exist.
## Examples
@@ -536,76 +799,142 @@ defmodule Cannery.Ammo do
%AmmoGroup{}
iex> get_ammo_group!(456, %User{id: 123})
- ** (Ecto.NoResultsError)
+ ** (KeyError)
"""
@spec get_ammo_group!(AmmoGroup.id(), User.t()) :: AmmoGroup.t()
- def get_ammo_group!(id, %User{id: user_id}) do
- Repo.one!(
+ def get_ammo_group!(id, user) do
+ [id] |> get_ammo_groups(user) |> Map.fetch!(id)
+ end
+
+ @doc """
+ Gets a group of ammo_groups by their ID.
+
+ ## Examples
+
+ iex> get_ammo_groups([123, 456], %User{id: 123})
+ %{123 => %AmmoGroup{}, 456 => %AmmoGroup{}}
+
+ """
+ @spec get_ammo_groups([AmmoGroup.id()], User.t()) ::
+ %{optional(AmmoGroup.id()) => AmmoGroup.t()}
+ def get_ammo_groups(ids, %User{id: user_id}) do
+ Repo.all(
from ag in AmmoGroup,
- left_join: sg in assoc(ag, :shot_groups),
- where: ag.id == ^id,
+ where: ag.id in ^ids,
where: ag.user_id == ^user_id,
- preload: [shot_groups: sg]
+ preload: ^@ammo_group_preloads,
+ select: {ag.id, ag}
)
- end
-
- @doc """
- Returns the number of shot rounds for an ammo group
- """
- @spec get_used_count(AmmoGroup.t()) :: non_neg_integer()
- def get_used_count(%AmmoGroup{} = ammo_group) do
- ammo_group
- |> Repo.preload(:shot_groups)
- |> Map.fetch!(:shot_groups)
- |> Enum.map(fn %{count: count} -> count end)
- |> Enum.sum()
- end
-
- @doc """
- Returns the last entered shot group for an ammo group
- """
- @spec get_last_used_shot_group(AmmoGroup.t()) :: ShotGroup.t() | nil
- def get_last_used_shot_group(%AmmoGroup{} = ammo_group) do
- ammo_group
- |> Repo.preload(:shot_groups)
- |> Map.fetch!(:shot_groups)
- |> Enum.max_by(fn %{date: date} -> date end, Date, fn -> nil end)
+ |> Map.new()
end
@doc """
Calculates the percentage remaining of an ammo group out of 100
+
+ ## Examples
+
+ iex> get_percentage_remaining(
+ ...> %AmmoGroup{id: 123, count: 5, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 100
+
"""
- @spec get_percentage_remaining(AmmoGroup.t()) :: non_neg_integer()
- def get_percentage_remaining(%AmmoGroup{count: 0}), do: 0
+ @spec get_percentage_remaining(AmmoGroup.t(), User.t()) :: non_neg_integer()
+ def get_percentage_remaining(%AmmoGroup{count: 0, user_id: user_id}, %User{id: user_id}) do
+ 0
+ end
- def get_percentage_remaining(%AmmoGroup{count: count} = ammo_group) do
- ammo_group = ammo_group |> Repo.preload(:shot_groups)
-
- shot_group_sum =
- ammo_group.shot_groups
- |> Enum.map(fn %{count: count} -> count end)
- |> Enum.sum()
-
- round(count / (count + shot_group_sum) * 100)
+ def get_percentage_remaining(%AmmoGroup{count: count} = ammo_group, current_user) do
+ round(count / get_original_count(ammo_group, current_user) * 100)
end
@doc """
Gets the original count for an ammo group
+
+ ## Examples
+
+ iex> get_original_count(
+ ...> %AmmoGroup{id: 123, count: 5, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 5
+
"""
- @spec get_original_count(AmmoGroup.t()) :: non_neg_integer()
- def get_original_count(%AmmoGroup{count: count} = ammo_group) do
- count + get_used_count(ammo_group)
+ @spec get_original_count(AmmoGroup.t(), User.t()) :: non_neg_integer()
+ def get_original_count(%AmmoGroup{id: ammo_group_id} = ammo_group, current_user) do
+ [ammo_group]
+ |> get_original_counts(current_user)
+ |> Map.fetch!(ammo_group_id)
+ end
+
+ @doc """
+ Gets the original counts for multiple ammo groups
+
+ ## Examples
+
+ iex> get_original_counts(
+ ...> [%AmmoGroup{id: 123, count: 5, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 5}
+
+ """
+ @spec get_original_counts([AmmoGroup.t()], User.t()) ::
+ %{optional(AmmoGroup.id()) => non_neg_integer()}
+ def get_original_counts(ammo_groups, %User{id: user_id} = current_user) do
+ used_counts = ActivityLog.get_used_counts(ammo_groups, current_user)
+
+ ammo_groups
+ |> Map.new(fn %AmmoGroup{id: ammo_group_id, count: count, user_id: ^user_id} ->
+ {ammo_group_id, count + Map.get(used_counts, ammo_group_id, 0)}
+ end)
end
@doc """
Calculates the CPR for a single ammo group
- """
- @spec get_cpr(AmmoGroup.t()) :: nil | float()
- def get_cpr(%AmmoGroup{price_paid: nil}), do: nil
- def get_cpr(%AmmoGroup{price_paid: price_paid} = ammo_group),
- do: calculate_cpr(price_paid, get_original_count(ammo_group))
+ ## Examples
+
+ iex> get_cpr(
+ ...> %AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456},
+ ...> %User{id: 456}
+ ...> )
+ 1
+
+ """
+ @spec get_cpr(AmmoGroup.t(), User.t()) :: float() | nil
+ def get_cpr(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
+ [ammo_group]
+ |> get_cprs(user)
+ |> Map.get(ammo_group_id)
+ end
+
+ @doc """
+ Calculates the CPR for multiple ammo groups
+
+ ## Examples
+
+ iex> get_cprs(
+ ...> [%AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456}],
+ ...> %User{id: 456}
+ ...> )
+ %{123 => 1}
+
+ """
+ @spec get_cprs([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => float()}
+ def get_cprs(ammo_groups, %User{id: user_id} = current_user) do
+ original_counts = get_original_counts(ammo_groups, current_user)
+
+ ammo_groups
+ |> Enum.reject(fn %AmmoGroup{price_paid: price_paid, user_id: ^user_id} ->
+ price_paid |> is_nil()
+ end)
+ |> Map.new(fn %{id: ammo_group_id, price_paid: price_paid} ->
+ {ammo_group_id, calculate_cpr(price_paid, Map.fetch!(original_counts, ammo_group_id))}
+ end)
+ end
@spec calculate_cpr(price_paid :: float() | nil, count :: integer()) :: float() | nil
defp calculate_cpr(nil, _count), do: nil
@@ -660,7 +989,7 @@ defmodule Cannery.Ammo do
returning: true
)
- {:ok, {count, inserted_ammo_groups}}
+ {:ok, {count, inserted_ammo_groups |> preload_ammo_group()}}
else
changesets
|> Enum.reject(fn %{valid?: valid} -> valid end)
@@ -674,7 +1003,7 @@ defmodule Cannery.Ammo do
_multiplier,
user
)
- when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do
+ when is_binary(ammo_type_id) and is_binary(container_id) do
changeset =
%AmmoGroup{}
|> AmmoGroup.create_changeset(
@@ -692,6 +1021,12 @@ defmodule Cannery.Ammo do
{:error, %AmmoGroup{} |> AmmoGroup.create_changeset(nil, nil, user, invalid_attrs)}
end
+ @spec preload_ammo_group(AmmoGroup.t()) :: AmmoGroup.t()
+ @spec preload_ammo_group([AmmoGroup.t()]) :: [AmmoGroup.t()]
+ defp preload_ammo_group(ammo_group_or_ammo_groups) do
+ ammo_group_or_ammo_groups |> Repo.preload(@ammo_group_preloads)
+ end
+
@doc """
Updates a ammo_group.
@@ -710,8 +1045,15 @@ defmodule Cannery.Ammo do
%AmmoGroup{user_id: user_id} = ammo_group,
attrs,
%User{id: user_id} = user
- ),
- do: ammo_group |> AmmoGroup.update_changeset(attrs, user) |> Repo.update()
+ ) do
+ ammo_group
+ |> AmmoGroup.update_changeset(attrs, user)
+ |> Repo.update()
+ |> case do
+ {:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
@doc """
Deletes a ammo_group.
@@ -727,8 +1069,14 @@ defmodule Cannery.Ammo do
"""
@spec delete_ammo_group(AmmoGroup.t(), User.t()) ::
{:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()}
- def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
- do: ammo_group |> Repo.delete()
+ def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}) do
+ ammo_group
+ |> Repo.delete()
+ |> case do
+ {:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
@doc """
Deletes a ammo_group.
@@ -740,6 +1088,8 @@ defmodule Cannery.Ammo do
"""
@spec delete_ammo_group!(AmmoGroup.t(), User.t()) :: AmmoGroup.t()
- def delete_ammo_group!(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
- do: ammo_group |> Repo.delete!()
+ def delete_ammo_group!(ammo_group, user) do
+ {:ok, ammo_group} = delete_ammo_group(ammo_group, user)
+ ammo_group
+ end
end
diff --git a/lib/cannery/ammo/ammo_group.ex b/lib/cannery/ammo/ammo_group.ex
index f5c7f5c5..676ead86 100644
--- a/lib/cannery/ammo/ammo_group.ex
+++ b/lib/cannery/ammo/ammo_group.ex
@@ -9,8 +9,8 @@ defmodule Cannery.Ammo.AmmoGroup do
use Ecto.Schema
import CanneryWeb.Gettext
import Ecto.Changeset
- alias Cannery.Ammo.{AmmoGroup, AmmoType}
- alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Containers, Containers.Container}
+ alias Cannery.Ammo.AmmoType
+ alias Cannery.{Accounts.User, Containers, Containers.Container}
alias Ecto.{Changeset, UUID}
@derive {Jason.Encoder,
@@ -33,15 +33,13 @@ defmodule Cannery.Ammo.AmmoGroup do
field :purchased_on, :date
belongs_to :ammo_type, AmmoType
- belongs_to :container, Container
- belongs_to :user, User
-
- has_many :shot_groups, ShotGroup
+ field :container_id, :binary_id
+ field :user_id, :binary_id
timestamps()
end
- @type t :: %AmmoGroup{
+ @type t :: %__MODULE__{
id: id(),
count: integer,
notes: String.t() | nil,
@@ -50,14 +48,12 @@ defmodule Cannery.Ammo.AmmoGroup do
purchased_on: Date.t(),
ammo_type: AmmoType.t() | nil,
ammo_type_id: AmmoType.id(),
- container: Container.t() | nil,
container_id: Container.id(),
- user: User.t() | nil,
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_ammo_group :: %AmmoGroup{}
+ @type new_ammo_group :: %__MODULE__{}
@type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_ammo_group())
@@ -76,8 +72,7 @@ defmodule Cannery.Ammo.AmmoGroup do
%User{id: user_id},
attrs
)
- when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) and
- not (user_id |> is_nil()) do
+ when is_binary(ammo_type_id) and is_binary(container_id) and is_binary(user_id) do
ammo_group
|> change(ammo_type_id: ammo_type_id)
|> change(user_id: user_id)
diff --git a/lib/cannery/ammo/ammo_type.ex b/lib/cannery/ammo/ammo_type.ex
index 9a2d7d16..c66b9a77 100644
--- a/lib/cannery/ammo/ammo_type.ex
+++ b/lib/cannery/ammo/ammo_type.ex
@@ -8,7 +8,7 @@ defmodule Cannery.Ammo.AmmoType do
use Ecto.Schema
import Ecto.Changeset
alias Cannery.Accounts.User
- alias Cannery.Ammo.{AmmoGroup, AmmoType}
+ alias Cannery.Ammo.AmmoGroup
alias Ecto.{Changeset, UUID}
@derive {Jason.Encoder,
@@ -64,14 +64,14 @@ defmodule Cannery.Ammo.AmmoType do
field :manufacturer, :string
field :upc, :string
- belongs_to :user, User
+ field :user_id, :binary_id
has_many :ammo_groups, AmmoGroup
timestamps()
end
- @type t :: %AmmoType{
+ @type t :: %__MODULE__{
id: id(),
name: String.t(),
desc: String.t() | nil,
@@ -95,12 +95,11 @@ defmodule Cannery.Ammo.AmmoType do
manufacturer: String.t() | nil,
upc: String.t() | nil,
user_id: User.id(),
- user: User.t() | nil,
ammo_groups: [AmmoGroup.t()] | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_ammo_type :: %AmmoType{}
+ @type new_ammo_type :: %__MODULE__{}
@type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_ammo_type())
diff --git a/lib/cannery/containers.ex b/lib/cannery/containers.ex
index 2d718316..20a66dd6 100644
--- a/lib/cannery/containers.ex
+++ b/lib/cannery/containers.ex
@@ -5,10 +5,12 @@ defmodule Cannery.Containers do
import CanneryWeb.Gettext
import Ecto.Query, warn: false
- alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo, Tags.Tag}
- alias Cannery.Containers.{Container, ContainerTag}
+ alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo}
+ alias Cannery.Containers.{Container, ContainerTag, Tag}
alias Ecto.Changeset
+ @container_preloads [:tags]
+
@doc """
Returns the list of containers.
@@ -28,11 +30,9 @@ defmodule Cannery.Containers do
as: :c,
left_join: t in assoc(c, :tags),
as: :t,
- left_join: ag in assoc(c, :ammo_groups),
- as: :ag,
where: c.user_id == ^user_id,
order_by: c.name,
- preload: [tags: t, ammo_groups: ag]
+ preload: ^@container_preloads
)
|> list_containers_search(search)
|> Repo.all()
@@ -106,12 +106,10 @@ defmodule Cannery.Containers do
def get_container!(id, %User{id: user_id}) do
Repo.one!(
from c in Container,
- left_join: t in assoc(c, :tags),
- left_join: ag in assoc(c, :ammo_groups),
where: c.user_id == ^user_id,
where: c.id == ^id,
order_by: c.name,
- preload: [tags: t, ammo_groups: ag]
+ preload: ^@container_preloads
)
end
@@ -130,7 +128,19 @@ defmodule Cannery.Containers do
@spec create_container(attrs :: map(), User.t()) ::
{:ok, Container.t()} | {:error, Container.changeset()}
def create_container(attrs, %User{} = user) do
- %Container{} |> Container.create_changeset(user, attrs) |> Repo.insert()
+ %Container{}
+ |> Container.create_changeset(user, attrs)
+ |> Repo.insert()
+ |> case do
+ {:ok, container} -> {:ok, container |> preload_container()}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
+
+ @spec preload_container(Container.t()) :: Container.t()
+ @spec preload_container([Container.t()]) :: [Container.t()]
+ def preload_container(container) do
+ container |> Repo.preload(@container_preloads)
end
@doc """
@@ -148,7 +158,13 @@ defmodule Cannery.Containers do
@spec update_container(Container.t(), User.t(), attrs :: map()) ::
{:ok, Container.t()} | {:error, Container.changeset()}
def update_container(%Container{user_id: user_id} = container, %User{id: user_id}, attrs) do
- container |> Container.update_changeset(attrs) |> Repo.update()
+ container
+ |> Container.update_changeset(attrs)
+ |> Repo.update()
+ |> case do
+ {:ok, container} -> {:ok, container |> preload_container()}
+ {:error, changeset} -> {:error, changeset}
+ end
end
@doc """
@@ -173,7 +189,12 @@ defmodule Cannery.Containers do
)
|> case do
0 ->
- container |> Repo.delete()
+ container
+ |> Repo.delete()
+ |> case do
+ {:ok, container} -> {:ok, container |> preload_container()}
+ {:error, changeset} -> {:error, changeset}
+ end
_amount ->
error = dgettext("errors", "Container must be empty before deleting")
@@ -214,8 +235,11 @@ defmodule Cannery.Containers do
%Container{user_id: user_id} = container,
%Tag{user_id: user_id} = tag,
%User{id: user_id}
- ),
- do: %ContainerTag{} |> ContainerTag.create_changeset(tag, container) |> Repo.insert!()
+ ) do
+ %ContainerTag{}
+ |> ContainerTag.create_changeset(tag, container)
+ |> Repo.insert!()
+ end
@doc """
Removes a tag from a container
@@ -226,45 +250,175 @@ defmodule Cannery.Containers do
%Container{}
"""
- @spec remove_tag!(Container.t(), Tag.t(), User.t()) :: non_neg_integer()
+ @spec remove_tag!(Container.t(), Tag.t(), User.t()) :: {non_neg_integer(), [ContainerTag.t()]}
def remove_tag!(
%Container{id: container_id, user_id: user_id},
%Tag{id: tag_id, user_id: user_id},
%User{id: user_id}
) do
- {count, _} =
+ {count, results} =
Repo.delete_all(
from ct in ContainerTag,
where: ct.container_id == ^container_id,
- where: ct.tag_id == ^tag_id
+ where: ct.tag_id == ^tag_id,
+ select: ct
)
- if count == 0, do: raise("could not delete container tag"), else: count
+ if count == 0, do: raise("could not delete container tag"), else: {count, results}
+ end
+
+ # Container Tags
+
+ @doc """
+ Returns the list of tags.
+
+ ## Examples
+
+ iex> list_tags(%User{id: 123})
+ [%Tag{}, ...]
+
+ iex> list_tags("cool", %User{id: 123})
+ [%Tag{name: "my cool tag"}, ...]
+
+ """
+ @spec list_tags(User.t()) :: [Tag.t()]
+ @spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()]
+ def list_tags(search \\ nil, user)
+
+ def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "",
+ do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name)
+
+ def list_tags(search, %{id: user_id}) when search |> is_binary() do
+ trimmed_search = String.trim(search)
+
+ Repo.all(
+ from t in Tag,
+ where: t.user_id == ^user_id,
+ where:
+ fragment(
+ "? @@ websearch_to_tsquery('english', ?)",
+ t.search,
+ ^trimmed_search
+ ),
+ order_by: {
+ :desc,
+ fragment(
+ "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
+ t.search,
+ ^trimmed_search
+ )
+ }
+ )
end
@doc """
- Returns number of rounds in container. If data is already preloaded, then
- there will be no db hit.
+ Gets a single tag.
+
+ ## Examples
+
+ iex> get_tag(123, %User{id: 123})
+ {:ok, %Tag{}}
+
+ iex> get_tag(456, %User{id: 123})
+ {:error, :not_found}
+
"""
- @spec get_container_ammo_group_count!(Container.t()) :: non_neg_integer()
- def get_container_ammo_group_count!(%Container{} = container) do
- container
- |> Repo.preload(:ammo_groups)
- |> Map.fetch!(:ammo_groups)
- |> Enum.reject(fn %{count: count} -> count == 0 end)
- |> Enum.count()
+ @spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, :not_found}
+ def get_tag(id, %User{id: user_id}) do
+ Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
+ |> case do
+ nil -> {:error, :not_found}
+ tag -> {:ok, tag}
+ end
end
@doc """
- Returns number of rounds in container. If data is already preloaded, then
- there will be no db hit.
+ Gets a single tag.
+
+ Raises `Ecto.NoResultsError` if the Tag does not exist.
+
+ ## Examples
+
+ iex> get_tag!(123, %User{id: 123})
+ %Tag{}
+
+ iex> get_tag!(456, %User{id: 123})
+ ** (Ecto.NoResultsError)
+
"""
- @spec get_container_rounds!(Container.t()) :: non_neg_integer()
- def get_container_rounds!(%Container{} = container) do
- container
- |> Repo.preload(:ammo_groups)
- |> Map.fetch!(:ammo_groups)
- |> Enum.map(fn %{count: count} -> count end)
- |> Enum.sum()
+ @spec get_tag!(Tag.id(), User.t()) :: Tag.t()
+ def get_tag!(id, %User{id: user_id}) do
+ Repo.one!(
+ from t in Tag,
+ where: t.id == ^id,
+ where: t.user_id == ^user_id
+ )
+ end
+
+ @doc """
+ Creates a tag.
+
+ ## Examples
+
+ iex> create_tag(%{field: value}, %User{id: 123})
+ {:ok, %Tag{}}
+
+ iex> create_tag(%{field: bad_value}, %User{id: 123})
+ {:error, %Changeset{}}
+
+ """
+ @spec create_tag(attrs :: map(), User.t()) ::
+ {:ok, Tag.t()} | {:error, Tag.changeset()}
+ def create_tag(attrs, %User{} = user) do
+ %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert()
+ end
+
+ @doc """
+ Updates a tag.
+
+ ## Examples
+
+ iex> update_tag(tag, %{field: new_value}, %User{id: 123})
+ {:ok, %Tag{}}
+
+ iex> update_tag(tag, %{field: bad_value}, %User{id: 123})
+ {:error, %Changeset{}}
+
+ """
+ @spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
+ {:ok, Tag.t()} | {:error, Tag.changeset()}
+ def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}) do
+ tag |> Tag.update_changeset(attrs) |> Repo.update()
+ end
+
+ @doc """
+ Deletes a tag.
+
+ ## Examples
+
+ iex> delete_tag(tag, %User{id: 123})
+ {:ok, %Tag{}}
+
+ iex> delete_tag(tag, %User{id: 123})
+ {:error, %Changeset{}}
+
+ """
+ @spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()}
+ def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}) do
+ tag |> Repo.delete()
+ end
+
+ @doc """
+ Deletes a tag.
+
+ ## Examples
+
+ iex> delete_tag!(tag, %User{id: 123})
+ %Tag{}
+
+ """
+ @spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
+ def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}) do
+ tag |> Repo.delete!()
end
end
diff --git a/lib/cannery/containers/container.ex b/lib/cannery/containers/container.ex
index cdf8b67f..790fb646 100644
--- a/lib/cannery/containers/container.ex
+++ b/lib/cannery/containers/container.ex
@@ -6,8 +6,7 @@ defmodule Cannery.Containers.Container do
use Ecto.Schema
import Ecto.Changeset
alias Ecto.{Changeset, UUID}
- alias Cannery.Containers.{Container, ContainerTag}
- alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag}
+ alias Cannery.{Accounts.User, Containers.ContainerTag, Containers.Tag}
@derive {Jason.Encoder,
only: [
@@ -26,28 +25,25 @@ defmodule Cannery.Containers.Container do
field :location, :string
field :type, :string
- belongs_to :user, User
+ field :user_id, :binary_id
- has_many :ammo_groups, AmmoGroup
many_to_many :tags, Tag, join_through: ContainerTag
timestamps()
end
- @type t :: %Container{
+ @type t :: %__MODULE__{
id: id(),
name: String.t(),
desc: String.t(),
location: String.t(),
type: String.t(),
- user: User.t(),
user_id: User.id(),
- ammo_groups: [AmmoGroup.t()] | nil,
tags: [Tag.t()] | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_container :: %Container{}
+ @type new_container :: %__MODULE__{}
@type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_container())
diff --git a/lib/cannery/containers/container_tag.ex b/lib/cannery/containers/container_tag.ex
index 4711fc8f..8a61cc09 100644
--- a/lib/cannery/containers/container_tag.ex
+++ b/lib/cannery/containers/container_tag.ex
@@ -1,12 +1,12 @@
defmodule Cannery.Containers.ContainerTag do
@moduledoc """
Thru-table struct for associating Cannery.Containers.Container and
- Cannery.Tags.Tag.
+ Cannery.Containers.Tag.
"""
use Ecto.Schema
import Ecto.Changeset
- alias Cannery.{Containers.Container, Containers.ContainerTag, Tags.Tag}
+ alias Cannery.Containers.{Container, Tag}
alias Ecto.{Changeset, UUID}
@primary_key {:id, :binary_id, autogenerate: true}
@@ -18,7 +18,7 @@ defmodule Cannery.Containers.ContainerTag do
timestamps()
end
- @type t :: %ContainerTag{
+ @type t :: %__MODULE__{
id: id(),
container: Container.t(),
container_id: Container.id(),
@@ -27,7 +27,7 @@ defmodule Cannery.Containers.ContainerTag do
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_container_tag :: %ContainerTag{}
+ @type new_container_tag :: %__MODULE__{}
@type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_container_tag())
diff --git a/lib/cannery/tags/tag.ex b/lib/cannery/containers/tag.ex
similarity index 89%
rename from lib/cannery/tags/tag.ex
rename to lib/cannery/containers/tag.ex
index 1dc4dc01..271440ac 100644
--- a/lib/cannery/tags/tag.ex
+++ b/lib/cannery/containers/tag.ex
@@ -1,4 +1,4 @@
-defmodule Cannery.Tags.Tag do
+defmodule Cannery.Containers.Tag do
@moduledoc """
Tags are added to containers to help organize, and can include custom-defined
text and bg colors.
@@ -6,8 +6,8 @@ defmodule Cannery.Tags.Tag do
use Ecto.Schema
import Ecto.Changeset
+ alias Cannery.Accounts.User
alias Ecto.{Changeset, UUID}
- alias Cannery.{Accounts.User, Tags.Tag}
@derive {Jason.Encoder,
only: [
@@ -23,22 +23,21 @@ defmodule Cannery.Tags.Tag do
field :bg_color, :string
field :text_color, :string
- belongs_to :user, User
+ field :user_id, :binary_id
timestamps()
end
- @type t :: %Tag{
+ @type t :: %__MODULE__{
id: id(),
name: String.t(),
bg_color: String.t(),
text_color: String.t(),
- user: User.t() | nil,
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
- @type new_tag() :: %Tag{}
+ @type new_tag() :: %__MODULE__{}
@type id() :: UUID.t()
@type changeset() :: Changeset.t(t() | new_tag())
diff --git a/lib/cannery/tags.ex b/lib/cannery/tags.ex
deleted file mode 100644
index 8fcda101..00000000
--- a/lib/cannery/tags.ex
+++ /dev/null
@@ -1,149 +0,0 @@
-defmodule Cannery.Tags do
- @moduledoc """
- The Tags context.
- """
-
- import Ecto.Query, warn: false
- import CanneryWeb.Gettext
- alias Cannery.{Accounts.User, Repo, Tags.Tag}
-
- @doc """
- Returns the list of tags.
-
- ## Examples
-
- iex> list_tags(%User{id: 123})
- [%Tag{}, ...]
-
- iex> list_tags("cool", %User{id: 123})
- [%Tag{name: "my cool tag"}, ...]
-
- """
- @spec list_tags(User.t()) :: [Tag.t()]
- @spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()]
- def list_tags(search \\ nil, user)
-
- def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "",
- do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name)
-
- def list_tags(search, %{id: user_id}) when search |> is_binary() do
- trimmed_search = String.trim(search)
-
- Repo.all(
- from t in Tag,
- where: t.user_id == ^user_id,
- where:
- fragment(
- "search @@ websearch_to_tsquery('english', ?)",
- ^trimmed_search
- ),
- order_by: {
- :desc,
- fragment(
- "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)",
- ^trimmed_search
- )
- }
- )
- end
-
- @doc """
- Gets a single tag.
-
- ## Examples
-
- iex> get_tag(123, %User{id: 123})
- {:ok, %Tag{}}
-
- iex> get_tag(456, %User{id: 123})
- {:error, "tag not found"}
-
- """
- @spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, String.t()}
- def get_tag(id, %User{id: user_id}) do
- Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
- |> case do
- nil -> {:error, dgettext("errors", "Tag not found")}
- tag -> {:ok, tag}
- end
- end
-
- @doc """
- Gets a single tag.
-
- Raises `Ecto.NoResultsError` if the Tag does not exist.
-
- ## Examples
-
- iex> get_tag!(123, %User{id: 123})
- %Tag{}
-
- iex> get_tag!(456, %User{id: 123})
- ** (Ecto.NoResultsError)
-
- """
- @spec get_tag!(Tag.id(), User.t()) :: Tag.t()
- def get_tag!(id, %User{id: user_id}),
- do: Repo.one!(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
-
- @doc """
- Creates a tag.
-
- ## Examples
-
- iex> create_tag(%{field: value}, %User{id: 123})
- {:ok, %Tag{}}
-
- iex> create_tag(%{field: bad_value}, %User{id: 123})
- {:error, %Changeset{}}
-
- """
- @spec create_tag(attrs :: map(), User.t()) ::
- {:ok, Tag.t()} | {:error, Tag.changeset()}
- def create_tag(attrs, %User{} = user),
- do: %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert()
-
- @doc """
- Updates a tag.
-
- ## Examples
-
- iex> update_tag(tag, %{field: new_value}, %User{id: 123})
- {:ok, %Tag{}}
-
- iex> update_tag(tag, %{field: bad_value}, %User{id: 123})
- {:error, %Changeset{}}
-
- """
- @spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
- {:ok, Tag.t()} | {:error, Tag.changeset()}
- def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}),
- do: tag |> Tag.update_changeset(attrs) |> Repo.update()
-
- @doc """
- Deletes a tag.
-
- ## Examples
-
- iex> delete_tag(tag, %User{id: 123})
- {:ok, %Tag{}}
-
- iex> delete_tag(tag, %User{id: 123})
- {:error, %Changeset{}}
-
- """
- @spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()}
- def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete()
-
- @doc """
- Deletes a tag.
-
- ## Examples
-
- iex> delete_tag!(tag, %User{id: 123})
- %Tag{}
-
- """
- @spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
- def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete!()
-end
diff --git a/lib/cannery_web/components/add_shot_group_component.ex b/lib/cannery_web/components/add_shot_group_component.ex
index 47dbae45..cd0b0ebe 100644
--- a/lib/cannery_web/components/add_shot_group_component.ex
+++ b/lib/cannery_web/components/add_shot_group_component.ex
@@ -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
diff --git a/lib/cannery_web/components/ammo_group_table_component.ex b/lib/cannery_web/components/ammo_group_table_component.ex
index 4bbd7bd2..4c165099 100644
--- a/lib/cannery_web/components/ammo_group_table_component.ex
+++ b/lib/cannery_web/components/ammo_group_table_component.ex
@@ -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
diff --git a/lib/cannery_web/components/ammo_type_table_component.ex b/lib/cannery_web/components/ammo_type_table_component.ex
index ef818f5b..ec36e765 100644
--- a/lib/cannery_web/components/ammo_type_table_component.ex
+++ b/lib/cannery_web/components/ammo_type_table_component.ex
@@ -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
diff --git a/lib/cannery_web/components/container_table_component.ex b/lib/cannery_web/components/container_table_component.ex
index f4393bf8..86379ff4 100644
--- a/lib/cannery_web/components/container_table_component.ex
+++ b/lib/cannery_web/components/container_table_component.ex
@@ -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"""
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
diff --git a/lib/cannery_web/components/core_components.ex b/lib/cannery_web/components/core_components.ex
index b72945c7..63904abb 100644
--- a/lib/cannery_web/components/core_components.ex
+++ b/lib/cannery_web/components/core_components.ex
@@ -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"""
-
-
- <%= @invite.name %>
-
-
- <%= if @invite.disabled_at |> is_nil() do %>
-
- <%= if @invite.uses_left do %>
- <%= gettext(
- "Uses Left: %{uses_left_count}",
- uses_left_count: @invite.uses_left
- ) %>
- <% else %>
- <%= gettext("Uses Left: Unlimited") %>
- <% end %>
-
- <% else %>
-
- <%= gettext("Invite Disabled") %>
-
- <% end %>
-
- <.qr_code
- content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
- filename={@invite.name}
- />
-
-
- <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
-
-
-
- <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %>
- <%= if @code_actions, do: render_slot(@code_actions) %>
-
-
-
- <%= render_slot(@inner_block) %>
-
-
- """
- 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 """
diff --git a/lib/cannery_web/components/core_components/ammo_group_card.html.heex b/lib/cannery_web/components/core_components/ammo_group_card.html.heex
index 7e69b823..554c03fc 100644
--- a/lib/cannery_web/components/core_components/ammo_group_card.html.heex
+++ b/lib/cannery_web/components/core_components/ammo_group_card.html.heex
@@ -17,12 +17,9 @@
<%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
-
Ammo.get_original_count() != @ammo_group.count}
- class="rounded-lg title text-lg"
- >
+
<%= gettext("Original Count:") %>
- <%= @ammo_group |> Ammo.get_original_count() %>
+ <%= @original_count %>
@@ -32,38 +29,35 @@
<%= gettext("Purchased on:") %>
- <.date date={@ammo_group.purchased_on} />
+ <.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} />
- Ammo.get_last_used_shot_group()} 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} />
- <%= if @ammo_group.price_paid do %>
-
- <%= gettext("Price paid:") %>
- <%= gettext("$%{amount}",
- amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
- ) %>
-
+
+ <%= gettext("Price paid:") %>
+ <%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %>
+
-
- <%= gettext("CPR:") %>
- <%= gettext("$%{amount}",
- amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
- ) %>
-
- <% end %>
+
+ <%= gettext("CPR:") %>
+ <%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
+
-
+
<%= 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 %>
diff --git a/lib/cannery_web/components/core_components/container_card.html.heex b/lib/cannery_web/components/core_components/container_card.html.heex
index a161420b..1b7f18fc 100644
--- a/lib/cannery_web/components/core_components/container_card.html.heex
+++ b/lib/cannery_web/components/core_components/container_card.html.heex
@@ -1,17 +1,17 @@
-
- <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
-
- <%= @container.name %>
-
-
+ <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
+
+ <%= @container.name %>
+
+
+
<%= gettext("Description:") %>
<%= @container.desc %>
@@ -27,20 +27,23 @@
<%= @container.location %>
- <%= unless @container.ammo_groups |> Enum.empty?() do %>
+ <%= if @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) != 0 do %>
<%= gettext("Packs:") %>
- <%= @container |> Containers.get_container_ammo_group_count!() %>
+ <%= @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) %>
<%= gettext("Rounds:") %>
- <%= @container |> Containers.get_container_rounds!() %>
+ <%= @container |> Ammo.get_round_count_for_container!(@current_user) %>
<% end %>
-
- <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
+
+ <.simple_tag_card :for={tag <- @container.tags} tag={tag} />
<%= if @tag_actions, do: render_slot(@tag_actions) %>
diff --git a/lib/cannery_web/components/core_components/date.html.heex b/lib/cannery_web/components/core_components/date.html.heex
index 5c638e0d..9240afae 100644
--- a/lib/cannery_web/components/core_components/date.html.heex
+++ b/lib/cannery_web/components/core_components/date.html.heex
@@ -1,12 +1,7 @@
-
Date.to_iso8601(:extended)}
- x-data={~s<{
+ 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) %>
diff --git a/lib/cannery_web/components/core_components/datetime.html.heex b/lib/cannery_web/components/core_components/datetime.html.heex
index c029fc07..8ea2bc3b 100644
--- a/lib/cannery_web/components/core_components/datetime.html.heex
+++ b/lib/cannery_web/components/core_components/datetime.html.heex
@@ -1,12 +1,7 @@
-
+ }/} x-text="datetime">
<%= cast_datetime(@datetime) %>
diff --git a/lib/cannery_web/components/core_components/invite_card.html.heex b/lib/cannery_web/components/core_components/invite_card.html.heex
new file mode 100644
index 00000000..34102e54
--- /dev/null
+++ b/lib/cannery_web/components/core_components/invite_card.html.heex
@@ -0,0 +1,46 @@
+
+
+ <%= @invite.name %>
+
+
+ <%= if @invite.disabled_at |> is_nil() do %>
+
+ <%= if @invite.uses_left do %>
+ <%= gettext(
+ "Uses Left: %{uses_left_count}",
+ uses_left_count: @invite.uses_left
+ ) %>
+ <% else %>
+ <%= gettext("Uses Left: Unlimited") %>
+ <% end %>
+
+ <% else %>
+
+ <%= gettext("Invite Disabled") %>
+
+ <% end %>
+
+ <.qr_code
+ content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
+ filename={@invite.name}
+ />
+
+
+ <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
+
+
+
+ <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %>
+ <%= if @code_actions, do: render_slot(@code_actions) %>
+
+
+
+ <%= render_slot(@inner_block) %>
+
+
diff --git a/lib/cannery_web/components/core_components/user_card.html.heex b/lib/cannery_web/components/core_components/user_card.html.heex
index 4cb90de0..1e502a45 100644
--- a/lib/cannery_web/components/core_components/user_card.html.heex
+++ b/lib/cannery_web/components/core_components/user_card.html.heex
@@ -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} />
diff --git a/lib/cannery_web/components/move_ammo_group_component.ex b/lib/cannery_web/components/move_ammo_group_component.ex
index 07c49e12..7adb1389 100644
--- a/lib/cannery_web/components/move_ammo_group_component.ex
+++ b/lib/cannery_web/components/move_ammo_group_component.ex
@@ -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
diff --git a/lib/cannery_web/components/shot_group_table_component.ex b/lib/cannery_web/components/shot_group_table_component.ex
index 347e7646..c58bf6a0 100644
--- a/lib/cannery_web/components/shot_group_table_component.ex
+++ b/lib/cannery_web/components/shot_group_table_component.ex
@@ -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 %>
-
- """
-
- {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 %>
+
+ """}
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
diff --git a/lib/cannery_web/components/table_component.ex b/lib/cannery_web/components/table_component.ex
index 91ba5479..9def80ce 100644
--- a/lib/cannery_web/components/table_component.ex
+++ b/lib/cannery_web/components/table_component.ex
@@ -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 ->
diff --git a/lib/cannery_web/controllers/export_controller.ex b/lib/cannery_web/controllers/export_controller.ex
index 3b62316f..60a0fa62 100644
--- a/lib/cannery_web/controllers/export_controller.ex
+++ b/lib/cannery_web/controllers/export_controller.ex
@@ -3,41 +3,49 @@ defmodule CanneryWeb.ExportController do
alias Cannery.{ActivityLog, Ammo, Containers}
def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do
- ammo_types =
- Ammo.list_ammo_types(current_user)
- |> Enum.map(fn ammo_type ->
- average_cost = ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user)
- round_count = ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
- used_count = ammo_type |> Ammo.get_used_count_for_ammo_type(current_user)
- ammo_group_count = ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true)
+ ammo_types = Ammo.list_ammo_types(current_user)
+ used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user)
+ round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user)
+ ammo_group_counts = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user)
+ total_ammo_group_counts =
+ ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true)
+
+ average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user)
+
+ ammo_types =
+ ammo_types
+ |> Enum.map(fn %{id: ammo_type_id} = ammo_type ->
ammo_type
|> Jason.encode!()
|> Jason.decode!()
|> Map.merge(%{
- "average_cost" => average_cost,
- "round_count" => round_count,
- "used_count" => used_count,
- "ammo_group_count" => ammo_group_count
+ "average_cost" => Map.get(average_costs, ammo_type_id),
+ "round_count" => Map.get(round_counts, ammo_type_id, 0),
+ "used_count" => Map.get(used_counts, ammo_type_id, 0),
+ "ammo_group_count" => Map.get(ammo_group_counts, ammo_type_id, 0),
+ "total_ammo_group_count" => Map.get(total_ammo_group_counts, ammo_type_id, 0)
})
end)
+ ammo_groups = Ammo.list_ammo_groups(nil, true, current_user)
+ used_counts = ammo_groups |> ActivityLog.get_used_counts(current_user)
+ original_counts = ammo_groups |> Ammo.get_original_counts(current_user)
+ cprs = ammo_groups |> Ammo.get_cprs(current_user)
+
ammo_groups =
- Ammo.list_ammo_groups(nil, true, current_user)
- |> Enum.map(fn ammo_group ->
- cpr = ammo_group |> Ammo.get_cpr()
- used_count = ammo_group |> Ammo.get_used_count()
- original_count = ammo_group |> Ammo.get_original_count()
- percentage_remaining = ammo_group |> Ammo.get_percentage_remaining()
+ ammo_groups
+ |> Enum.map(fn %{id: ammo_group_id} = ammo_group ->
+ percentage_remaining = ammo_group |> Ammo.get_percentage_remaining(current_user)
ammo_group
|> Jason.encode!()
|> Jason.decode!()
|> Map.merge(%{
- "used_count" => used_count,
+ "used_count" => Map.get(used_counts, ammo_group_id),
"percentage_remaining" => percentage_remaining,
- "original_count" => original_count,
- "cpr" => cpr
+ "original_count" => Map.get(original_counts, ammo_group_id),
+ "cpr" => Map.get(cprs, ammo_group_id)
})
end)
@@ -46,8 +54,8 @@ defmodule CanneryWeb.ExportController do
containers =
Containers.list_containers(current_user)
|> Enum.map(fn container ->
- ammo_group_count = container |> Containers.get_container_ammo_group_count!()
- round_count = container |> Containers.get_container_rounds!()
+ ammo_group_count = container |> Ammo.get_ammo_groups_count_for_container!(current_user)
+ round_count = container |> Ammo.get_round_count_for_container!(current_user)
container
|> Jason.encode!()
diff --git a/lib/cannery_web/controllers/user_registration_controller.ex b/lib/cannery_web/controllers/user_registration_controller.ex
index 6e231ebe..c5c58a21 100644
--- a/lib/cannery_web/controllers/user_registration_controller.ex
+++ b/lib/cannery_web/controllers/user_registration_controller.ex
@@ -3,6 +3,7 @@ defmodule CanneryWeb.UserRegistrationController do
import CanneryWeb.Gettext
alias Cannery.{Accounts, Accounts.Invites}
alias CanneryWeb.{Endpoint, HomeLive}
+ alias Ecto.Changeset
def new(conn, %{"invite" => invite_token}) do
if Invites.valid_invite_token?(invite_token) do
@@ -70,7 +71,7 @@ defmodule CanneryWeb.UserRegistrationController do
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
- {:error, %Ecto.Changeset{} = changeset} ->
+ {:error, %Changeset{} = changeset} ->
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
end
end
diff --git a/lib/cannery_web/live/ammo_group_live/form_component.ex b/lib/cannery_web/live/ammo_group_live/form_component.ex
index 2235423f..510628f8 100644
--- a/lib/cannery_web/live/ammo_group_live/form_component.ex
+++ b/lib/cannery_web/live/ammo_group_live/form_component.ex
@@ -44,7 +44,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
@impl true
def handle_event("validate", %{"ammo_group" => ammo_group_params}, socket) do
- {:noreply, socket |> assign_changeset(ammo_group_params)}
+ {:noreply, socket |> assign_changeset(ammo_group_params, :validate)}
end
def handle_event(
@@ -56,6 +56,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
end
# HTML Helpers
+
@spec container_options([Container.t()]) :: [{String.t(), Container.id()}]
defp container_options(containers) do
containers |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
@@ -70,35 +71,28 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
defp assign_changeset(
%{assigns: %{action: action, ammo_group: ammo_group, current_user: user}} = socket,
- ammo_group_params
+ ammo_group_params,
+ changeset_action \\ nil
) do
- changeset_action =
- cond do
- action in [:new, :clone] -> :insert
- action == :edit -> :update
+ default_action =
+ case action do
+ create when create in [:new, :clone] -> :insert
+ :edit -> :update
end
changeset =
- cond do
- action in [:new, :clone] ->
- ammo_type =
- if ammo_group_params |> Map.has_key?("ammo_type_id"),
- do: ammo_group_params |> Map.get("ammo_type_id") |> Ammo.get_ammo_type!(user),
- else: nil
-
- container =
- if ammo_group_params |> Map.has_key?("container_id"),
- do: ammo_group_params |> Map.get("container_id") |> Containers.get_container!(user),
- else: nil
-
+ case default_action do
+ :insert ->
+ ammo_type = maybe_get_ammo_type(ammo_group_params, user)
+ container = maybe_get_container(ammo_group_params, user)
ammo_group |> AmmoGroup.create_changeset(ammo_type, container, user, ammo_group_params)
- action == :edit ->
+ :update ->
ammo_group |> AmmoGroup.update_changeset(ammo_group_params, user)
end
changeset =
- case changeset |> Changeset.apply_action(changeset_action) do
+ case changeset |> Changeset.apply_action(changeset_action || default_action) do
{:ok, _data} -> changeset
{:error, changeset} -> changeset
end
@@ -106,6 +100,20 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
socket |> assign(:changeset, changeset)
end
+ defp maybe_get_container(%{"container_id" => container_id}, user)
+ when is_binary(container_id) do
+ container_id |> Containers.get_container!(user)
+ end
+
+ defp maybe_get_container(_params_not_found, _user), do: nil
+
+ defp maybe_get_ammo_type(%{"ammo_type_id" => ammo_type_id}, user)
+ when is_binary(ammo_type_id) do
+ ammo_type_id |> Ammo.get_ammo_type!(user)
+ end
+
+ defp maybe_get_ammo_type(_params_not_found, _user), do: nil
+
defp save_ammo_group(
%{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} =
socket,
@@ -146,27 +154,26 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
multiplier: multiplier
)
- {:error, changeset} =
- changeset
- |> Changeset.add_error(:multiplier, error_msg)
- |> Changeset.apply_action(:insert)
-
- socket |> assign(:changeset, changeset)
+ save_multiplier_error(socket, changeset, error_msg)
:error ->
error_msg = dgettext("errors", "Could not parse number of copies")
-
- {:error, changeset} =
- changeset
- |> Changeset.add_error(:multiplier, error_msg)
- |> Changeset.apply_action(:insert)
-
- socket |> assign(:changeset, changeset)
+ save_multiplier_error(socket, changeset, error_msg)
end
{:noreply, socket}
end
+ @spec save_multiplier_error(Socket.t(), Changeset.t(), String.t()) :: Socket.t()
+ defp save_multiplier_error(socket, changeset, error_msg) do
+ {:error, changeset} =
+ changeset
+ |> Changeset.add_error(:multiplier, error_msg)
+ |> Changeset.apply_action(:insert)
+
+ socket |> assign(:changeset, changeset)
+ end
+
defp create_multiple(
%{assigns: %{current_user: current_user, return_to: return_to}} = socket,
ammo_group_params,
diff --git a/lib/cannery_web/live/ammo_group_live/index.html.heex b/lib/cannery_web/live/ammo_group_live/index.html.heex
index c3ece5d5..8aad4972 100644
--- a/lib/cannery_web/live/ammo_group_live/index.html.heex
+++ b/lib/cannery_web/live/ammo_group_live/index.html.heex
@@ -104,7 +104,7 @@
- <:container :let={%{container: %{name: container_name} = container} = ammo_group}>
+ <:container :let={{ammo_group, %{name: container_name} = container}}>
<.link
navigate={Routes.container_show_path(Endpoint, :show, container)}
@@ -117,18 +117,18 @@
patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)}
class="mx-2 my-1 text-sm btn btn-primary"
>
- <%= gettext("Move ammo") %>
+ <%= dgettext("actions", "Move ammo") %>
- <:actions :let={ammo_group}>
+ <:actions :let={%{count: ammo_group_count} = ammo_group}>
<.link
navigate={Routes.ammo_group_show_path(Endpoint, :show, ammo_group)}
class="text-primary-600 link"
aria-label={
- gettext("View ammo group of %{ammo_group_count} bullets",
- ammo_group_count: ammo_group.count
+ dgettext("actions", "View ammo group of %{ammo_group_count} bullets",
+ ammo_group_count: ammo_group_count
)
}
>
@@ -139,8 +139,8 @@
patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)}
class="text-primary-600 link"
aria-label={
- gettext("Edit ammo group of %{ammo_group_count} bullets",
- ammo_group_count: ammo_group.count
+ dgettext("actions", "Edit ammo group of %{ammo_group_count} bullets",
+ ammo_group_count: ammo_group_count
)
}
>
@@ -151,8 +151,8 @@
patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)}
class="text-primary-600 link"
aria-label={
- gettext("Clone ammo group of %{ammo_group_count} bullets",
- ammo_group_count: ammo_group.count
+ dgettext("actions", "Clone ammo group of %{ammo_group_count} bullets",
+ ammo_group_count: ammo_group_count
)
}
>
@@ -166,8 +166,8 @@
phx-value-id={ammo_group.id}
data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
aria-label={
- gettext("Delete ammo group of %{ammo_group_count} bullets",
- ammo_group_count: ammo_group.count
+ dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets",
+ ammo_group_count: ammo_group_count
)
}
>
@@ -179,8 +179,8 @@
<% end %>
-<%= cond do %>
- <% @live_action in [:new, :edit, :clone] -> %>
+<%= case @live_action do %>
+ <% create when create in [:new, :edit, :clone] -> %>
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
<.live_component
module={CanneryWeb.AmmoGroupLive.FormComponent}
@@ -192,7 +192,7 @@
current_user={@current_user}
/>
- <% @live_action == :add_shot_group -> %>
+ <% :add_shot_group -> %>
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
<.live_component
module={CanneryWeb.Components.AddShotGroupComponent}
@@ -204,7 +204,7 @@
current_user={@current_user}
/>
- <% @live_action == :move -> %>
+ <% :move -> %>
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
<.live_component
module={CanneryWeb.Components.MoveAmmoGroupComponent}
@@ -216,6 +216,5 @@
current_user={@current_user}
/>
- <% true -> %>
- <%= nil %>
+ <% _ -> %>
<% end %>
diff --git a/lib/cannery_web/live/ammo_group_live/show.ex b/lib/cannery_web/live/ammo_group_live/show.ex
index d5557301..4ce0fde3 100644
--- a/lib/cannery_web/live/ammo_group_live/show.ex
+++ b/lib/cannery_web/live/ammo_group_live/show.ex
@@ -4,7 +4,9 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
"""
use CanneryWeb, :live_view
- alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
+ alias Cannery.{ActivityLog, ActivityLog.ShotGroup}
+ alias Cannery.{Ammo, Ammo.AmmoGroup}
+ alias Cannery.Containers
alias CanneryWeb.Endpoint
alias Phoenix.LiveView.Socket
@@ -81,30 +83,45 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
end
@spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t()
- defp display_ammo_group(socket, %AmmoGroup{} = ammo_group) do
- ammo_group = ammo_group |> Repo.preload([:container, :ammo_type, :shot_groups], force: true)
-
+ defp display_ammo_group(
+ %{assigns: %{current_user: current_user}} = socket,
+ %AmmoGroup{container_id: container_id} = ammo_group
+ ) do
columns = [
%{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}
]
+ shot_groups = ActivityLog.list_shot_groups_for_ammo_group(ammo_group, current_user)
+
rows =
- ammo_group.shot_groups
+ shot_groups
|> Enum.map(fn shot_group ->
ammo_group |> get_table_row_for_shot_group(shot_group, columns)
end)
- socket |> assign(ammo_group: ammo_group, columns: columns, rows: rows)
+ socket
+ |> assign(
+ ammo_group: ammo_group,
+ original_count: Ammo.get_original_count(ammo_group, current_user),
+ percentage_remaining: Ammo.get_percentage_remaining(ammo_group, current_user),
+ container: container_id && Containers.get_container!(container_id, current_user),
+ shot_groups: shot_groups,
+ columns: columns,
+ rows: rows
+ )
end
defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id),
do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user))
+ @spec display_currency(float()) :: String.t()
+ defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
+
@spec get_table_row_for_shot_group(AmmoGroup.t(), ShotGroup.t(), [map()]) :: map()
- defp get_table_row_for_shot_group(ammo_group, %{date: date} = shot_group, columns) do
+ defp get_table_row_for_shot_group(ammo_group, %{id: id, date: date} = shot_group, columns) do
assigns = %{ammo_group: ammo_group, shot_group: shot_group}
columns
@@ -112,11 +129,11 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
value =
case key do
:date ->
- assigns = %{date: date}
+ assigns = %{id: id, date: date}
{date,
~H"""
- <.date date={@date} />
+ <.date id={"#{@id}-date"} date={@date} />
"""}
:actions ->
diff --git a/lib/cannery_web/live/ammo_group_live/show.html.heex b/lib/cannery_web/live/ammo_group_live/show.html.heex
index 90f17f22..46de99e7 100644
--- a/lib/cannery_web/live/ammo_group_live/show.html.heex
+++ b/lib/cannery_web/live/ammo_group_live/show.html.heex
@@ -11,12 +11,12 @@
<%= gettext("Original count:") %>
- <%= Ammo.get_original_count(@ammo_group) %>
+ <%= @original_count %>
<%= gettext("Percentage left:") %>
- <%= gettext("%{percentage}%", percentage: @ammo_group |> Ammo.get_percentage_remaining()) %>
+ <%= gettext("%{percentage}%", percentage: @percentage_remaining) %>
<%= if @ammo_group.notes do %>
@@ -28,23 +28,19 @@
<%= gettext("Purchased on:") %>
- <.date date={@ammo_group.purchased_on} />
+ <.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} />
<%= if @ammo_group.price_paid do %>
<%= gettext("Original cost:") %>
- <%= gettext("$%{amount}",
- amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
- ) %>
+ <%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %>
<%= gettext("Current value:") %>
<%= gettext("$%{amount}",
- amount:
- (@ammo_group.price_paid * Ammo.get_percentage_remaining(@ammo_group) / 100)
- |> :erlang.float_to_binary(decimals: 2)
+ amount: display_currency(@ammo_group.price_paid * @percentage_remaining / 100)
) %>
<% end %>
@@ -77,7 +73,7 @@
phx-click="delete"
data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
aria-label={
- gettext("Delete ammo group of %{ammo_group_count} bullets",
+ dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets",
ammo_group_count: @ammo_group.count
)
}
@@ -97,7 +93,7 @@
patch={Routes.ammo_group_show_path(Endpoint, :move, @ammo_group)}
class="btn btn-primary"
>
- <%= dgettext("actions", "Move containers") %>
+ <%= dgettext("actions", "Move ammo") %>
<.link
@@ -112,18 +108,18 @@
- <%= if @ammo_group.container do %>
+ <%= if @container do %>
<%= gettext("Stored in") %>
- <.container_card container={@ammo_group.container} />
+ <.container_card container={@container} current_user={@current_user} />
<% else %>
<%= gettext("This ammo is not in a container") %>
<% end %>
- <%= unless @ammo_group.shot_groups |> Enum.empty?() do %>
+ <%= unless @shot_groups |> Enum.empty?() do %>
diff --git a/lib/cannery_web/live/ammo_type_live/form_component.ex b/lib/cannery_web/live/ammo_type_live/form_component.ex
index 955dbb13..f4a41fe8 100644
--- a/lib/cannery_web/live/ammo_type_live/form_component.ex
+++ b/lib/cannery_web/live/ammo_type_live/form_component.ex
@@ -35,15 +35,18 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
ammo_type_params
) do
changeset_action =
- cond do
- action in [:new, :clone] -> :insert
- action == :edit -> :update
+ case action do
+ create when create in [:new, :clone] -> :insert
+ :edit -> :update
end
changeset =
- cond do
- action in [:new, :clone] -> ammo_type |> AmmoType.create_changeset(user, ammo_type_params)
- action == :edit -> ammo_type |> AmmoType.update_changeset(ammo_type_params)
+ case action do
+ create when create in [:new, :clone] ->
+ ammo_type |> AmmoType.create_changeset(user, ammo_type_params)
+
+ :edit ->
+ ammo_type |> AmmoType.update_changeset(ammo_type_params)
end
changeset =
diff --git a/lib/cannery_web/live/ammo_type_live/index.ex b/lib/cannery_web/live/ammo_type_live/index.ex
index efc033f4..d8ef33c4 100644
--- a/lib/cannery_web/live/ammo_type_live/index.ex
+++ b/lib/cannery_web/live/ammo_type_live/index.ex
@@ -69,9 +69,7 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
@impl true
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
%{name: name} = Ammo.get_ammo_type!(id, current_user) |> Ammo.delete_ammo_type!(current_user)
-
prompt = dgettext("prompts", "%{name} deleted succesfully", name: name)
-
{:noreply, socket |> put_flash(:info, prompt) |> list_ammo_types()}
end
@@ -84,8 +82,8 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
- {:noreply,
- socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :search, search_term))}
+ search_path = Routes.ammo_type_index_path(Endpoint, :search, search_term)
+ {:noreply, socket |> push_patch(to: search_path)}
end
defp list_ammo_types(%{assigns: %{search: search, current_user: current_user}} = socket) do
diff --git a/lib/cannery_web/live/ammo_type_live/show.ex b/lib/cannery_web/live/ammo_type_live/show.ex
index e2590f93..e88573ff 100644
--- a/lib/cannery_web/live/ammo_type_live/show.ex
+++ b/lib/cannery_web/live/ammo_type_live/show.ex
@@ -4,7 +4,7 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
"""
use CanneryWeb, :live_view
- alias Cannery.{Ammo, Ammo.AmmoType}
+ alias Cannery.{ActivityLog, Ammo, Ammo.AmmoType}
alias CanneryWeb.Endpoint
@fields_list [
@@ -91,12 +91,27 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
ammo_type |> Map.get(field) != default_value
end)
+ ammo_groups = ammo_type |> Ammo.list_ammo_groups_for_type(current_user, show_used)
+ original_counts = ammo_groups |> Ammo.get_original_counts(current_user)
+ cprs = ammo_groups |> Ammo.get_cprs(current_user)
+ historical_packs_count = ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true)
+ last_used_dates = ammo_groups |> ActivityLog.get_last_used_dates(current_user)
+
socket
|> assign(
page_title: page_title(live_action, ammo_type),
ammo_type: ammo_type,
- ammo_groups: ammo_type |> Ammo.list_ammo_groups_for_type(current_user, show_used),
- avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user),
+ ammo_groups: ammo_groups,
+ original_counts: original_counts,
+ cprs: cprs,
+ last_used_dates: last_used_dates,
+ avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type(current_user),
+ rounds: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user),
+ used_rounds: ammo_type |> ActivityLog.get_used_count_for_ammo_type(current_user),
+ historical_round_count: ammo_type |> Ammo.get_historical_count_for_ammo_type(current_user),
+ packs_count: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user),
+ used_packs_count: ammo_type |> Ammo.get_used_ammo_groups_count_for_type(current_user),
+ historical_packs_count: historical_packs_count,
fields_list: @fields_list,
fields_to_display: fields_to_display
)
@@ -110,6 +125,9 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
socket |> display_ammo_type(ammo_type)
end
+ @spec display_currency(float()) :: String.t()
+ defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
+
defp page_title(action, %{name: ammo_type_name}) when action in [:show, :table],
do: ammo_type_name
diff --git a/lib/cannery_web/live/ammo_type_live/show.html.heex b/lib/cannery_web/live/ammo_type_live/show.html.heex
index 4b6ab684..2edef8b2 100644
--- a/lib/cannery_web/live/ammo_type_live/show.html.heex
+++ b/lib/cannery_web/live/ammo_type_live/show.html.heex
@@ -6,8 +6,8 @@
<%= @ammo_type.desc %>
@@ -71,7 +71,7 @@
- <%= @ammo_type |> Ammo.get_round_count_for_ammo_type(@current_user) %>
+ <%= @rounds %>
@@ -79,7 +79,7 @@
- <%= @ammo_type |> Ammo.get_used_count_for_ammo_type(@current_user) %>
+ <%= @used_rounds %>
@@ -87,7 +87,7 @@
- <%= @ammo_type |> Ammo.get_historical_count_for_ammo_type(@current_user) %>
+ <%= @historical_round_count %>
@@ -99,7 +99,7 @@
- <%= @ammo_type |> Ammo.get_ammo_groups_count_for_type(@current_user) %>
+ <%= @packs_count %>
@@ -107,7 +107,7 @@
- <%= @ammo_type |> Ammo.get_used_ammo_groups_count_for_type(@current_user) %>
+ <%= @used_packs_count %>
@@ -115,7 +115,7 @@
- <%= @ammo_type |> Ammo.get_ammo_groups_count_for_type(@current_user, true) %>
+ <%= @historical_packs_count %>
@@ -127,7 +127,7 @@
- <.datetime datetime={@ammo_type.inserted_at} />
+ <.datetime id={"#{@ammo_type.id}-inserted-at"} datetime={@ammo_type.inserted_at} />
<%= if @avg_cost_per_round do %>
@@ -136,9 +136,7 @@
- <%= gettext("$%{amount}",
- amount: @avg_cost_per_round |> :erlang.float_to_binary(decimals: 2)
- ) %>
+ <%= gettext("$%{amount}", amount: display_currency(@avg_cost_per_round)) %>
<% else %>
@@ -177,7 +175,7 @@
ammo_groups={@ammo_groups}
current_user={@current_user}
>
- <:container :let={%{container: %{name: container_name} = container}}>
+ <:container :let={{_ammo_group, %{name: container_name} = container}}>
<.link
navigate={Routes.container_show_path(Endpoint, :show, container)}
class="mx-2 my-1 link"
@@ -189,8 +187,12 @@
<% else %>
<.ammo_group_card
- :for={ammo_group <- @ammo_groups}
+ :for={%{id: ammo_group_id} = ammo_group <- @ammo_groups}
ammo_group={ammo_group}
+ original_count={Map.fetch!(@original_counts, ammo_group_id)}
+ cpr={Map.get(@cprs, ammo_group_id)}
+ last_used_date={Map.get(@last_used_dates, ammo_group_id)}
+ current_user={@current_user}
show_container={true}
/>
diff --git a/lib/cannery_web/live/container_live/edit_tags_component.ex b/lib/cannery_web/live/container_live/edit_tags_component.ex
index f00d1223..7e206862 100644
--- a/lib/cannery_web/live/container_live/edit_tags_component.ex
+++ b/lib/cannery_web/live/container_live/edit_tags_component.ex
@@ -4,7 +4,8 @@ defmodule CanneryWeb.ContainerLive.EditTagsComponent do
"""
use CanneryWeb, :live_component
- alias Cannery.{Accounts.User, Containers, Containers.Container, Tags, Tags.Tag}
+ alias Cannery.{Accounts.User, Containers}
+ alias Cannery.Containers.{Container, Tag}
alias Phoenix.LiveView.Socket
@impl true
@@ -22,7 +23,7 @@ defmodule CanneryWeb.ContainerLive.EditTagsComponent do
assigns,
socket
) do
- tags = Tags.list_tags(current_user)
+ tags = Containers.list_tags(current_user)
{:ok, socket |> assign(assigns) |> assign(:tags, tags)}
end
diff --git a/lib/cannery_web/live/container_live/form_component.ex b/lib/cannery_web/live/container_live/form_component.ex
index 6b249d28..4a5fc49b 100644
--- a/lib/cannery_web/live/container_live/form_component.ex
+++ b/lib/cannery_web/live/container_live/form_component.ex
@@ -35,17 +35,17 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
container_params
) do
changeset_action =
- cond do
- action in [:new, :clone] -> :insert
- action == :edit -> :update
+ case action do
+ create when create in [:new, :clone] -> :insert
+ :edit -> :update
end
changeset =
- cond do
- action in [:new, :clone] ->
+ case action do
+ create when create in [:new, :clone] ->
container |> Container.create_changeset(user, container_params)
- action == :edit ->
+ :edit ->
container |> Container.update_changeset(container_params)
end
diff --git a/lib/cannery_web/live/container_live/index.ex b/lib/cannery_web/live/container_live/index.ex
index 37f4852e..07084e4b 100644
--- a/lib/cannery_web/live/container_live/index.ex
+++ b/lib/cannery_web/live/container_live/index.ex
@@ -4,7 +4,7 @@ defmodule CanneryWeb.ContainerLive.Index do
"""
use CanneryWeb, :live_view
- alias Cannery.{Containers, Containers.Container, Repo}
+ alias Cannery.{Containers, Containers.Container}
alias Ecto.Changeset
@impl true
@@ -22,10 +22,7 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
- %{name: container_name} =
- container =
- Containers.get_container!(id, current_user)
- |> Repo.preload([:tags, :ammo_groups])
+ %{name: container_name} = container = Containers.get_container!(id, current_user)
socket
|> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
@@ -61,9 +58,7 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
- %{name: container_name} =
- container =
- Containers.get_container!(id, current_user) |> Repo.preload([:tags, :ammo_groups])
+ %{name: container_name} = container = Containers.get_container!(id, current_user)
page_title = gettext("Edit %{name} tags", name: container_name)
socket |> assign(page_title: page_title, container: container)
@@ -119,10 +114,6 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do
- containers =
- Containers.list_containers(search, current_user)
- |> Repo.preload([:tags, :ammo_groups])
-
- socket |> assign(:containers, containers)
+ socket |> assign(:containers, Containers.list_containers(search, current_user))
end
end
diff --git a/lib/cannery_web/live/container_live/index.html.heex b/lib/cannery_web/live/container_live/index.html.heex
index 084da1ab..c8603e4e 100644
--- a/lib/cannery_web/live/container_live/index.html.heex
+++ b/lib/cannery_web/live/container_live/index.html.heex
@@ -108,7 +108,11 @@
<% else %>
- <.container_card :for={container <- @containers} container={container}>
+ <.container_card
+ :for={container <- @containers}
+ container={container}
+ current_user={@current_user}
+ >
<:tag_actions>
<.link
@@ -162,29 +166,30 @@
<% end %>
-<.modal
- :if={@live_action in [:new, :edit, :clone]}
- return_to={Routes.container_index_path(Endpoint, :index)}
->
- <.live_component
- module={CanneryWeb.ContainerLive.FormComponent}
- id={@container.id || :new}
- title={@page_title}
- action={@live_action}
- container={@container}
- return_to={Routes.container_index_path(Endpoint, :index)}
- current_user={@current_user}
- />
-
-
-<.modal :if={@live_action == :edit_tags} return_to={Routes.container_index_path(Endpoint, :index)}>
- <.live_component
- module={CanneryWeb.ContainerLive.EditTagsComponent}
- id={@container.id}
- title={@page_title}
- action={@live_action}
- container={@container}
- current_path={Routes.container_index_path(Endpoint, :edit_tags, @container)}
- current_user={@current_user}
- />
-
+<%= case @live_action do %>
+ <% modifying when modifying in [:new, :edit, :clone] -> %>
+ <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
+ <.live_component
+ module={CanneryWeb.ContainerLive.FormComponent}
+ id={@container.id || :new}
+ title={@page_title}
+ action={@live_action}
+ container={@container}
+ return_to={Routes.container_index_path(Endpoint, :index)}
+ current_user={@current_user}
+ />
+
+ <% :edit_tags -> %>
+ <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
+ <.live_component
+ module={CanneryWeb.ContainerLive.EditTagsComponent}
+ id={@container.id}
+ title={@page_title}
+ action={@live_action}
+ container={@container}
+ current_path={Routes.container_index_path(Endpoint, :edit_tags, @container)}
+ current_user={@current_user}
+ />
+
+ <% _ -> %>
+<% end %>
diff --git a/lib/cannery_web/live/container_live/show.ex b/lib/cannery_web/live/container_live/show.ex
index ec64cc2e..cce64731 100644
--- a/lib/cannery_web/live/container_live/show.ex
+++ b/lib/cannery_web/live/container_live/show.ex
@@ -4,7 +4,7 @@ defmodule CanneryWeb.ContainerLive.Show do
"""
use CanneryWeb, :live_view
- alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container, Repo, Tags}
+ alias Cannery.{Accounts.User, ActivityLog, Ammo, Containers, Containers.Container}
alias CanneryWeb.Endpoint
alias Ecto.Changeset
alias Phoenix.LiveView.Socket
@@ -30,7 +30,7 @@ defmodule CanneryWeb.ContainerLive.Show do
%{assigns: %{container: container, current_user: current_user}} = socket
) do
socket =
- case Tags.get_tag(tag_id, current_user) do
+ case Containers.get_tag(tag_id, current_user) do
{:ok, tag} ->
_count = Containers.remove_tag!(container, tag, current_user)
@@ -42,8 +42,8 @@ defmodule CanneryWeb.ContainerLive.Show do
socket |> put_flash(:info, prompt) |> render_container()
- {:error, error_string} ->
- socket |> put_flash(:error, error_string)
+ {:error, :not_found} ->
+ socket |> put_flash(:error, dgettext("errors", "Tag not found"))
end
{:noreply, socket}
@@ -96,12 +96,11 @@ defmodule CanneryWeb.ContainerLive.Show do
id,
current_user
) do
- %{name: container_name} =
- container =
- Containers.get_container!(id, current_user)
- |> Repo.preload([:tags], force: true)
-
+ %{name: container_name} = container = Containers.get_container!(id, current_user)
ammo_groups = Ammo.list_ammo_groups_for_container(container, current_user, show_used)
+ original_counts = ammo_groups |> Ammo.get_original_counts(current_user)
+ cprs = ammo_groups |> Ammo.get_cprs(current_user)
+ last_used_dates = ammo_groups |> ActivityLog.get_last_used_dates(current_user)
page_title =
case live_action do
@@ -110,7 +109,16 @@ defmodule CanneryWeb.ContainerLive.Show do
:edit_tags -> gettext("Edit %{name} tags", name: container_name)
end
- socket |> assign(container: container, ammo_groups: ammo_groups, page_title: page_title)
+ socket
+ |> assign(
+ container: container,
+ round_count: Ammo.get_round_count_for_container!(container, current_user),
+ ammo_groups: ammo_groups,
+ original_counts: original_counts,
+ cprs: cprs,
+ last_used_dates: last_used_dates,
+ page_title: page_title
+ )
end
@spec render_container(Socket.t()) :: Socket.t()
diff --git a/lib/cannery_web/live/container_live/show.html.heex b/lib/cannery_web/live/container_live/show.html.heex
index b26e6b86..1f2a6a9b 100644
--- a/lib/cannery_web/live/container_live/show.html.heex
+++ b/lib/cannery_web/live/container_live/show.html.heex
@@ -20,21 +20,18 @@
<%= unless @ammo_groups |> Enum.empty?() do %>
- <%= if @show_used do %>
- <%= gettext("Total packs:") %>
- <% else %>
- <%= gettext("Packs:") %>
- <% end %>
+ <%= gettext("Packs:") %>
+ <%= @ammo_groups |> Enum.reject(fn %{count: count} -> count in [0, nil] end) |> Enum.count() %>
+
+
+
+ <%= gettext("Total packs:") %>
<%= Enum.count(@ammo_groups) %>
- <%= if @show_used do %>
- <%= gettext("Total rounds:") %>
- <% else %>
- <%= gettext("Rounds:") %>
- <% end %>
- <%= @container |> Containers.get_container_rounds!() %>
+ <%= gettext("Rounds:") %>
+ <%= @round_count %>
<% end %>
@@ -130,40 +127,45 @@
<% else %>
- <.ammo_group_card :for={ammo_group <- @ammo_groups} ammo_group={ammo_group} />
+ <.ammo_group_card
+ :for={%{id: ammo_group_id} = ammo_group <- @ammo_groups}
+ ammo_group={ammo_group}
+ original_count={Map.fetch!(@original_counts, ammo_group_id)}
+ cpr={Map.get(@cprs, ammo_group_id)}
+ last_used_date={Map.get(@last_used_dates, ammo_group_id)}
+ current_user={@current_user}
+ />
<% end %>
<% end %>
-<.modal
- :if={@live_action == :edit}
- return_to={Routes.container_show_path(Endpoint, :show, @container)}
->
- <.live_component
- module={CanneryWeb.ContainerLive.FormComponent}
- id={@container.id}
- title={@page_title}
- action={@live_action}
- container={@container}
- return_to={Routes.container_show_path(Endpoint, :show, @container)}
- current_user={@current_user}
- />
-
-
-<.modal
- :if={@live_action == :edit_tags}
- return_to={Routes.container_show_path(Endpoint, :show, @container)}
->
- <.live_component
- module={CanneryWeb.ContainerLive.EditTagsComponent}
- id={@container.id}
- title={@page_title}
- action={@live_action}
- container={@container}
- return_to={Routes.container_show_path(Endpoint, :show, @container)}
- current_path={Routes.container_show_path(Endpoint, :edit_tags, @container)}
- current_user={@current_user}
- />
-
+<%= case @live_action do %>
+ <% :edit -> %>
+ <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
+ <.live_component
+ module={CanneryWeb.ContainerLive.FormComponent}
+ id={@container.id}
+ title={@page_title}
+ action={@live_action}
+ container={@container}
+ return_to={Routes.container_show_path(Endpoint, :show, @container)}
+ current_user={@current_user}
+ />
+
+ <% :edit_tags -> %>
+ <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
+ <.live_component
+ module={CanneryWeb.ContainerLive.EditTagsComponent}
+ id={@container.id}
+ title={@page_title}
+ action={@live_action}
+ container={@container}
+ return_to={Routes.container_show_path(Endpoint, :show, @container)}
+ current_path={Routes.container_show_path(Endpoint, :edit_tags, @container)}
+ current_user={@current_user}
+ />
+
+ <% _ -> %>
+<% end %>
diff --git a/lib/cannery_web/live/home_live.ex b/lib/cannery_web/live/home_live.ex
index b1f8e160..1829acec 100644
--- a/lib/cannery_web/live/home_live.ex
+++ b/lib/cannery_web/live/home_live.ex
@@ -12,7 +12,6 @@ defmodule CanneryWeb.HomeLive do
@impl true
def mount(_params, _session, socket) do
admins = Accounts.list_users_by_role(:admin)
- socket = socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)
- {:ok, socket}
+ {:ok, socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)}
end
end
diff --git a/lib/cannery_web/live/home_live.html.heex b/lib/cannery_web/live/home_live.html.heex
index ba207df0..6ffd7267 100644
--- a/lib/cannery_web/live/home_live.html.heex
+++ b/lib/cannery_web/live/home_live.html.heex
@@ -17,8 +17,7 @@
-
+
<%= gettext("Easy to Use:") %>
@@ -37,8 +36,7 @@
<%= gettext("Your data stays with you, period") %>
-
+
<%= gettext("Simple:") %>
@@ -81,9 +79,9 @@
<%= gettext("Registration:") %>
- <%= case Application.get_env(:cannery, Cannery.Accounts)[:registration] do
- "public" -> gettext("Public Signups")
- _ -> gettext("Invite Only")
+ <%= case Accounts.registration_mode() do
+ :public -> gettext("Public Signups")
+ :invite_only -> gettext("Invite Only")
end %>
diff --git a/lib/cannery_web/live/invite_live/index.ex b/lib/cannery_web/live/invite_live/index.ex
index 507d978f..3bf9516b 100644
--- a/lib/cannery_web/live/invite_live/index.ex
+++ b/lib/cannery_web/live/invite_live/index.ex
@@ -29,8 +29,8 @@ defmodule CanneryWeb.InviteLive.Index do
end
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
- socket
- |> assign(page_title: gettext("Edit Invite"), invite: Invites.get_invite!(id, current_user))
+ invite = Invites.get_invite!(id, current_user)
+ socket |> assign(page_title: gettext("Edit Invite"), invite: invite)
end
defp apply_action(socket, :new, _params) do
@@ -123,8 +123,7 @@ defmodule CanneryWeb.InviteLive.Index do
end
def handle_event("copy_to_clipboard", _params, socket) do
- prompt = dgettext("prompts", "Copied to clipboard")
- {:noreply, socket |> put_flash(:info, prompt)}
+ {:noreply, socket |> put_flash(:info, dgettext("prompts", "Copied to clipboard"))}
end
def handle_event(
@@ -133,9 +132,7 @@ defmodule CanneryWeb.InviteLive.Index do
%{assigns: %{current_user: current_user}} = socket
) do
%{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user)
-
prompt = dgettext("prompts", "%{user_email} deleted succesfully", user_email: user_email)
-
{:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
end
@@ -148,7 +145,8 @@ defmodule CanneryWeb.InviteLive.Index do
|> Map.get(:admin, [])
|> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end)
+ use_counts = invites |> Invites.get_use_counts(current_user)
users = all_users |> Map.get(:user, [])
- socket |> assign(invites: invites, admins: admins, users: users)
+ socket |> assign(invites: invites, use_counts: use_counts, admins: admins, users: users)
end
end
diff --git a/lib/cannery_web/live/invite_live/index.html.heex b/lib/cannery_web/live/invite_live/index.html.heex
index c58d8147..ac4b722d 100644
--- a/lib/cannery_web/live/invite_live/index.html.heex
+++ b/lib/cannery_web/live/invite_live/index.html.heex
@@ -19,7 +19,12 @@
<% end %>
- <.invite_card :for={invite <- @invites} invite={invite} current_user={@current_user}>
+ <.invite_card
+ :for={invite <- @invites}
+ invite={invite}
+ current_user={@current_user}
+ use_count={Map.get(@use_counts, invite.id)}
+ >
<:code_actions>