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 7a10079..52a6d62 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 0adb6d2..6cf6efc 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 7f8e955..93c0f30 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 3d9aa35..d24bbf0 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 f4a66ad..32d7e2c 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 ece49a8..3760e8c 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 f5c7f5c..676ead8 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 9a2d7d1..c66b9a7 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 2d71831..20a66dd 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 cdf8b67..790fb64 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 4711fc8..8a61cc0 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 1dc4dc0..271440a 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 8fcda10..0000000 --- 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 47dbae4..cd0b0eb 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 4bbd7bd..4c16509 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 ef818f5..ec36e76 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 f4393bf..86379ff 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 b72945c..63904ab 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 7e69b82..554c03f 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 a161420..1b7f18f 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 5c638e0..9240afa 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 @@ -
- <: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 d555730..4ce0fde 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 90f17f2..46de99e 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 955dbb1..f4a41fe 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 efc033f..d8ef33c 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 e2590f9..e88573f 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 4b6ab68..2edef8b 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 f00d122..7e20686 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 6b249d2..4a5fc49 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 37f4852..07084e4 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 084da1a..c8603e4 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 ec64cc2..cce6473 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 b26e6b8..1f2a6a9 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 b1f8e16..1829ace 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 ba207df..6ffd726 100644 --- a/lib/cannery_web/live/home_live.html.heex +++ b/lib/cannery_web/live/home_live.html.heex @@ -17,8 +17,7 @@