add selectable ammo types
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		| @@ -6,65 +6,92 @@ defmodule Cannery.ActivityLog do | ||||
|   import Ecto.Query, warn: false | ||||
|   alias Cannery.Ammo.{AmmoGroup, AmmoType} | ||||
|   alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo} | ||||
|   alias Ecto.Multi | ||||
|   alias Ecto.{Multi, Queryable} | ||||
|  | ||||
|   @doc """ | ||||
|   Returns the list of shot_groups. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> list_shot_groups(%User{id: 123}) | ||||
|       iex> list_shot_groups(:all, %User{id: 123}) | ||||
|       [%ShotGroup{}, ...] | ||||
|  | ||||
|       iex> list_shot_groups("cool", %User{id: 123}) | ||||
|       iex> list_shot_groups("cool", :all, %User{id: 123}) | ||||
|       [%ShotGroup{notes: "My cool shot group"}, ...] | ||||
|  | ||||
|       iex> list_shot_groups("cool", :rifle, %User{id: 123}) | ||||
|       [%ShotGroup{notes: "Shot some rifle rounds"}, ...] | ||||
|  | ||||
|   """ | ||||
|   @spec list_shot_groups(User.t()) :: [ShotGroup.t()] | ||||
|   @spec list_shot_groups(search :: nil | String.t(), User.t()) :: [ShotGroup.t()] | ||||
|   def list_shot_groups(search \\ nil, user) | ||||
|   @spec list_shot_groups(AmmoType.type() | :all, User.t()) :: [ShotGroup.t()] | ||||
|   @spec list_shot_groups(search :: nil | String.t(), AmmoType.type() | :all, User.t()) :: | ||||
|           [ShotGroup.t()] | ||||
|   def list_shot_groups(search \\ nil, type, %{id: user_id}) do | ||||
|     from(sg in ShotGroup, | ||||
|       as: :sg, | ||||
|       left_join: ag in AmmoGroup, | ||||
|       as: :ag, | ||||
|       on: sg.ammo_group_id == ag.id, | ||||
|       left_join: at in AmmoType, | ||||
|       as: :at, | ||||
|       on: ag.ammo_type_id == at.id, | ||||
|       where: sg.user_id == ^user_id, | ||||
|       distinct: sg.id | ||||
|     ) | ||||
|     |> list_shot_groups_search(search) | ||||
|     |> list_shot_groups_filter_type(type) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_shot_groups(search, %{id: user_id}) when search |> is_nil() or search == "", | ||||
|     do: Repo.all(from sg in ShotGroup, where: sg.user_id == ^user_id) | ||||
|   @spec list_shot_groups_search(Queryable.t(), search :: String.t() | nil) :: | ||||
|           Queryable.t() | ||||
|   defp list_shot_groups_search(query, search) when search in ["", nil], do: query | ||||
|  | ||||
|   def list_shot_groups(search, %{id: user_id}) when search |> is_binary() do | ||||
|   defp list_shot_groups_search(query, search) when search |> is_binary() do | ||||
|     trimmed_search = String.trim(search) | ||||
|  | ||||
|     Repo.all( | ||||
|       from sg in ShotGroup, | ||||
|         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( | ||||
|             "? @@ websearch_to_tsquery('english', ?)", | ||||
|             sg.search, | ||||
|             ^trimmed_search | ||||
|           ) or | ||||
|             fragment( | ||||
|               "? @@ websearch_to_tsquery('english', ?)", | ||||
|               ag.search, | ||||
|               ^trimmed_search | ||||
|             ) or | ||||
|             fragment( | ||||
|               "? @@ websearch_to_tsquery('english', ?)", | ||||
|               at.search, | ||||
|               ^trimmed_search | ||||
|             ), | ||||
|         order_by: { | ||||
|           :desc, | ||||
|           fragment( | ||||
|             "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||
|             sg.search, | ||||
|             ^trimmed_search | ||||
|           ) | ||||
|         }, | ||||
|         distinct: sg.id | ||||
|     query | ||||
|     |> where( | ||||
|       [sg: sg, ag: ag, at: at], | ||||
|       fragment( | ||||
|         "? @@ websearch_to_tsquery('english', ?)", | ||||
|         sg.search, | ||||
|         ^trimmed_search | ||||
|       ) or | ||||
|         fragment( | ||||
|           "? @@ websearch_to_tsquery('english', ?)", | ||||
|           ag.search, | ||||
|           ^trimmed_search | ||||
|         ) or | ||||
|         fragment( | ||||
|           "? @@ websearch_to_tsquery('english', ?)", | ||||
|           at.search, | ||||
|           ^trimmed_search | ||||
|         ) | ||||
|     ) | ||||
|     |> order_by([sg: sg], { | ||||
|       :desc, | ||||
|       fragment( | ||||
|         "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||
|         sg.search, | ||||
|         ^trimmed_search | ||||
|       ) | ||||
|     }) | ||||
|   end | ||||
|  | ||||
|   @spec list_shot_groups_filter_type(Queryable.t(), AmmoType.type() | :all) :: | ||||
|           Queryable.t() | ||||
|   defp list_shot_groups_filter_type(query, :rifle), | ||||
|     do: query |> where([at: at], at.type == :rifle) | ||||
|  | ||||
|   defp list_shot_groups_filter_type(query, :pistol), | ||||
|     do: query |> where([at: at], at.type == :pistol) | ||||
|  | ||||
|   defp list_shot_groups_filter_type(query, :shotgun), | ||||
|     do: query |> where([at: at], at.type == :shotgun) | ||||
|  | ||||
|   defp list_shot_groups_filter_type(query, _all), do: query | ||||
|  | ||||
|   @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}, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ defmodule Cannery.Ammo do | ||||
|   alias Cannery.Containers.{Container, ContainerTag, Tag} | ||||
|   alias Cannery.{ActivityLog, ActivityLog.ShotGroup} | ||||
|   alias Cannery.Ammo.{AmmoGroup, AmmoType} | ||||
|   alias Ecto.Changeset | ||||
|   alias Ecto.{Changeset, Queryable} | ||||
|  | ||||
|   @ammo_group_create_limit 10_000 | ||||
|   @ammo_group_preloads [:ammo_type] | ||||
| @@ -20,50 +20,69 @@ defmodule Cannery.Ammo do | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> list_ammo_types(%User{id: 123}) | ||||
|       iex> list_ammo_types(%User{id: 123}, :all) | ||||
|       [%AmmoType{}, ...] | ||||
|  | ||||
|       iex> list_ammo_types("cool", %User{id: 123}) | ||||
|       [%AmmoType{name: "My cool ammo type"}, ...] | ||||
|       iex> list_ammo_types("cool", %User{id: 123}, :shotgun) | ||||
|       [%AmmoType{name: "My cool ammo type", type: :shotgun}, ...] | ||||
|  | ||||
|   """ | ||||
|   @spec list_ammo_types(User.t()) :: [AmmoType.t()] | ||||
|   @spec list_ammo_types(search :: nil | String.t(), User.t()) :: [AmmoType.t()] | ||||
|   def list_ammo_types(search \\ nil, user) | ||||
|   @spec list_ammo_types(User.t(), AmmoType.type() | :all) :: [AmmoType.t()] | ||||
|   @spec list_ammo_types(search :: nil | String.t(), User.t(), AmmoType.type() | :all) :: | ||||
|           [AmmoType.t()] | ||||
|   def list_ammo_types(search \\ nil, user, type) | ||||
|  | ||||
|   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 | ||||
|   def list_ammo_types(search, %{id: user_id}, type) do | ||||
|     from(at in AmmoType, | ||||
|       as: :at, | ||||
|       where: at.user_id == ^user_id, | ||||
|       preload: ^@ammo_type_preloads | ||||
|     ) | ||||
|     |> list_ammo_types_filter_type(type) | ||||
|     |> list_ammo_types_filter_search(search) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_ammo_types(search, %{id: user_id}) when search |> is_binary() do | ||||
|   @spec list_ammo_types_filter_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t() | ||||
|   defp list_ammo_types_filter_search(query, search) when search in ["", nil], | ||||
|     do: query |> order_by([at: at], at.name) | ||||
|  | ||||
|   defp list_ammo_types_filter_search(query, search) when search |> is_binary() do | ||||
|     trimmed_search = String.trim(search) | ||||
|  | ||||
|     Repo.all( | ||||
|       from at in AmmoType, | ||||
|         where: at.user_id == ^user_id, | ||||
|         where: | ||||
|           fragment( | ||||
|             "? @@ websearch_to_tsquery('english', ?)", | ||||
|             at.search, | ||||
|             ^trimmed_search | ||||
|           ), | ||||
|         order_by: { | ||||
|           :desc, | ||||
|           fragment( | ||||
|             "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||
|             at.search, | ||||
|             ^trimmed_search | ||||
|           ) | ||||
|         }, | ||||
|         preload: ^@ammo_type_preloads | ||||
|     query | ||||
|     |> where( | ||||
|       [at: at], | ||||
|       fragment( | ||||
|         "? @@ websearch_to_tsquery('english', ?)", | ||||
|         at.search, | ||||
|         ^trimmed_search | ||||
|       ) | ||||
|     ) | ||||
|     |> order_by( | ||||
|       [at: at], | ||||
|       { | ||||
|         :desc, | ||||
|         fragment( | ||||
|           "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||
|           at.search, | ||||
|           ^trimmed_search | ||||
|         ) | ||||
|       } | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   @spec list_ammo_types_filter_type(Queryable.t(), AmmoType.type() | :all) :: Queryable.t() | ||||
|   defp list_ammo_types_filter_type(query, :rifle), do: query |> where([at: at], at.type == :rifle) | ||||
|  | ||||
|   defp list_ammo_types_filter_type(query, :pistol), | ||||
|     do: query |> where([at: at], at.type == :pistol) | ||||
|  | ||||
|   defp list_ammo_types_filter_type(query, :shotgun), | ||||
|     do: query |> where([at: at], at.type == :shotgun) | ||||
|  | ||||
|   defp list_ammo_types_filter_type(query, _all), do: query | ||||
|  | ||||
|   @doc """ | ||||
|   Returns a count of ammo_types. | ||||
|  | ||||
| @@ -80,7 +99,7 @@ defmodule Cannery.Ammo do | ||||
|         where: at.user_id == ^user_id, | ||||
|         select: count(at.id), | ||||
|         distinct: true | ||||
|     ) | ||||
|     ) || 0 | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
| @@ -375,36 +394,31 @@ defmodule Cannery.Ammo do | ||||
|  | ||||
|   """ | ||||
|   @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()) :: | ||||
|   @spec list_ammo_groups_for_type(AmmoType.t(), User.t(), show_used :: boolean()) :: | ||||
|           [AmmoGroup.t()] | ||||
|   def list_ammo_groups_for_type(ammo_type, user, include_empty \\ false) | ||||
|   def list_ammo_groups_for_type(ammo_type, user, show_used \\ false) | ||||
|  | ||||
|   def list_ammo_groups_for_type( | ||||
|         %AmmoType{id: ammo_type_id, user_id: user_id}, | ||||
|         %User{id: user_id}, | ||||
|         true = _include_empty | ||||
|         show_used | ||||
|       ) do | ||||
|     Repo.all( | ||||
|       from ag in AmmoGroup, | ||||
|         where: ag.ammo_type_id == ^ammo_type_id, | ||||
|         where: ag.user_id == ^user_id, | ||||
|         preload: ^@ammo_group_preloads | ||||
|     from(ag in AmmoGroup, | ||||
|       as: :ag, | ||||
|       where: ag.ammo_type_id == ^ammo_type_id, | ||||
|       where: ag.user_id == ^user_id, | ||||
|       preload: ^@ammo_group_preloads | ||||
|     ) | ||||
|     |> list_ammo_groups_for_type_show_used(show_used) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_ammo_groups_for_type( | ||||
|         %AmmoType{id: ammo_type_id, user_id: user_id}, | ||||
|         %User{id: user_id}, | ||||
|         false = _include_empty | ||||
|       ) do | ||||
|     Repo.all( | ||||
|       from ag in AmmoGroup, | ||||
|         where: ag.ammo_type_id == ^ammo_type_id, | ||||
|         where: ag.user_id == ^user_id, | ||||
|         where: not (ag.count == 0), | ||||
|         preload: ^@ammo_group_preloads | ||||
|     ) | ||||
|   end | ||||
|   @spec list_ammo_groups_for_type_show_used(Queryable.t(), show_used :: boolean()) :: | ||||
|           Queryable.t() | ||||
|   def list_ammo_groups_for_type_show_used(query, false), | ||||
|     do: query |> where([ag: ag], ag.count > 0) | ||||
|  | ||||
|   def list_ammo_groups_for_type_show_used(query, _true), do: query | ||||
|  | ||||
|   @doc """ | ||||
|   Returns the list of ammo_groups for a user and container. | ||||
| @@ -413,50 +427,86 @@ defmodule Cannery.Ammo do | ||||
|  | ||||
|       iex> list_ammo_groups_for_container( | ||||
|       ...>   %Container{id: 123, user_id: 456}, | ||||
|       ...>   :rifle, | ||||
|       ...>   %User{id: 456} | ||||
|       ...> ) | ||||
|       [%AmmoGroup{}, ...] | ||||
|  | ||||
|       iex> list_ammo_groups_for_container( | ||||
|       ...>   %Container{id: 123, user_id: 456}, | ||||
|       ...>   %User{id: 456}, | ||||
|       ...>   true | ||||
|       ...>   :all, | ||||
|       ...>   %User{id: 456} | ||||
|       ...> ) | ||||
|       [%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()) :: | ||||
|           [AmmoGroup.t()] | ||||
|   def list_ammo_groups_for_container(container, user, include_empty \\ false) | ||||
|  | ||||
|   @spec list_ammo_groups_for_container( | ||||
|           Container.t(), | ||||
|           AmmoType.t() | :all, | ||||
|           User.t() | ||||
|         ) :: [AmmoGroup.t()] | ||||
|   def list_ammo_groups_for_container( | ||||
|         %Container{id: container_id, user_id: user_id}, | ||||
|         %User{id: user_id}, | ||||
|         true = _include_empty | ||||
|         type, | ||||
|         %User{id: user_id} | ||||
|       ) do | ||||
|     Repo.all( | ||||
|       from ag in AmmoGroup, | ||||
|         where: ag.container_id == ^container_id, | ||||
|         where: ag.user_id == ^user_id, | ||||
|         preload: ^@ammo_group_preloads | ||||
|     from(ag in AmmoGroup, | ||||
|       as: :ag, | ||||
|       join: at in assoc(ag, :ammo_type), | ||||
|       as: :at, | ||||
|       where: ag.container_id == ^container_id, | ||||
|       where: ag.user_id == ^user_id, | ||||
|       where: ag.count > 0, | ||||
|       preload: ^@ammo_group_preloads | ||||
|     ) | ||||
|     |> list_ammo_groups_for_container_filter_type(type) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_ammo_groups_for_container( | ||||
|         %Container{id: container_id, user_id: user_id}, | ||||
|         %User{id: user_id}, | ||||
|         false = _include_empty | ||||
|       ) do | ||||
|     Repo.all( | ||||
|       from ag in AmmoGroup, | ||||
|         where: ag.container_id == ^container_id, | ||||
|         where: ag.user_id == ^user_id, | ||||
|         where: not (ag.count == 0), | ||||
|         preload: ^@ammo_group_preloads | ||||
|   @spec list_ammo_groups_for_container_filter_type(Queryable.t(), AmmoType.type() | :all) :: | ||||
|           Queryable.t() | ||||
|   defp list_ammo_groups_for_container_filter_type(query, :rifle), | ||||
|     do: query |> where([at: at], at.type == :rifle) | ||||
|  | ||||
|   defp list_ammo_groups_for_container_filter_type(query, :pistol), | ||||
|     do: query |> where([at: at], at.type == :pistol) | ||||
|  | ||||
|   defp list_ammo_groups_for_container_filter_type(query, :shotgun), | ||||
|     do: query |> where([at: at], at.type == :shotgun) | ||||
|  | ||||
|   defp list_ammo_groups_for_container_filter_type(query, _all), do: query | ||||
|  | ||||
|   @doc """ | ||||
|   Returns a count of ammo_groups. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> get_ammo_groups_count!(%User{id: 123}) | ||||
|       3 | ||||
|  | ||||
|       iex> get_ammo_groups_count!(%User{id: 123}, true) | ||||
|       4 | ||||
|  | ||||
|   """ | ||||
|   @spec get_ammo_groups_count!(User.t()) :: integer() | ||||
|   @spec get_ammo_groups_count!(User.t(), show_used :: boolean()) :: integer() | ||||
|   def get_ammo_groups_count!(%User{id: user_id}, show_used \\ false) do | ||||
|     from(ag in AmmoGroup, | ||||
|       as: :ag, | ||||
|       where: ag.user_id == ^user_id, | ||||
|       select: count(ag.id), | ||||
|       distinct: true | ||||
|     ) | ||||
|     |> get_ammo_groups_count_show_used(show_used) | ||||
|     |> Repo.one() || 0 | ||||
|   end | ||||
|  | ||||
|   @spec get_ammo_groups_count_show_used(Queryable.t(), show_used :: boolean()) :: Queryable.t() | ||||
|   defp get_ammo_groups_count_show_used(query, false), | ||||
|     do: query |> where([ag: ag], ag.count > 0) | ||||
|  | ||||
|   defp get_ammo_groups_count_show_used(query, _true), do: query | ||||
|  | ||||
|   @doc """ | ||||
|   Returns the count of ammo_groups for an ammo type. | ||||
|  | ||||
| @@ -477,15 +527,15 @@ defmodule Cannery.Ammo do | ||||
|  | ||||
|   """ | ||||
|   @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()) :: | ||||
|   @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t(), show_used :: boolean()) :: | ||||
|           non_neg_integer() | ||||
|   def get_ammo_groups_count_for_type( | ||||
|         %AmmoType{id: ammo_type_id} = ammo_type, | ||||
|         user, | ||||
|         include_empty \\ false | ||||
|         show_used \\ false | ||||
|       ) do | ||||
|     [ammo_type] | ||||
|     |> get_ammo_groups_count_for_types(user, include_empty) | ||||
|     |> get_ammo_groups_count_for_types(user, show_used) | ||||
|     |> Map.get(ammo_type_id, 0) | ||||
|   end | ||||
|  | ||||
| @@ -510,28 +560,31 @@ defmodule Cannery.Ammo do | ||||
|   """ | ||||
|   @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()) :: | ||||
|   @spec get_ammo_groups_count_for_types([AmmoType.t()], User.t(), show_used :: boolean()) :: | ||||
|           %{optional(AmmoType.id()) => non_neg_integer()} | ||||
|   def get_ammo_groups_count_for_types(ammo_types, %User{id: user_id}, include_empty \\ false) do | ||||
|   def get_ammo_groups_count_for_types(ammo_types, %User{id: user_id}, show_used \\ 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, | ||||
|       as: :ag, | ||||
|       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) | ||||
|     |> get_ammo_groups_count_for_types_maybe_show_used(show_used) | ||||
|     |> Repo.all() | ||||
|     |> Map.new() | ||||
|   end | ||||
|  | ||||
|   defp maybe_include_empty(query, true), do: query | ||||
|   @spec get_ammo_groups_count_for_types_maybe_show_used(Queryable.t(), show_used :: boolean()) :: | ||||
|           Queryable.t() | ||||
|   defp get_ammo_groups_count_for_types_maybe_show_used(query, true), do: query | ||||
|  | ||||
|   defp maybe_include_empty(query, _false) do | ||||
|     query |> where([ag], not (ag.count == 0)) | ||||
|   defp get_ammo_groups_count_for_types_maybe_show_used(query, _false) do | ||||
|     query |> where([ag: ag], not (ag.count == 0)) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
| @@ -628,7 +681,7 @@ defmodule Cannery.Ammo do | ||||
|     Repo.all( | ||||
|       from ag in AmmoGroup, | ||||
|         where: ag.container_id in ^container_ids, | ||||
|         where: ag.count != 0, | ||||
|         where: ag.count > 0, | ||||
|         group_by: ag.container_id, | ||||
|         select: {ag.container_id, count(ag.id)} | ||||
|     ) | ||||
| @@ -690,17 +743,20 @@ defmodule Cannery.Ammo do | ||||
|       iex> list_ammo_groups(%User{id: 123}) | ||||
|       [%AmmoGroup{}, ...] | ||||
|  | ||||
|       iex> list_ammo_groups("cool", true, %User{id: 123}) | ||||
|       iex> list_ammo_groups("cool", %User{id: 123}, true) | ||||
|       [%AmmoGroup{notes: "My cool ammo group"}, ...] | ||||
|  | ||||
|   """ | ||||
|   @spec list_ammo_groups(User.t()) :: [AmmoGroup.t()] | ||||
|   @spec list_ammo_groups(search :: nil | String.t(), User.t()) :: [AmmoGroup.t()] | ||||
|   @spec list_ammo_groups(search :: nil | String.t(), include_empty :: boolean(), User.t()) :: | ||||
|   @spec list_ammo_groups(search :: String.t() | nil, AmmoType.type() | :all, User.t()) :: | ||||
|           [AmmoGroup.t()] | ||||
|   def list_ammo_groups(search \\ nil, include_empty \\ false, %{id: user_id}) do | ||||
|     from( | ||||
|       ag in AmmoGroup, | ||||
|   @spec list_ammo_groups( | ||||
|           search :: nil | String.t(), | ||||
|           AmmoType.type() | :all, | ||||
|           User.t(), | ||||
|           show_used :: boolean() | ||||
|         ) :: [AmmoGroup.t()] | ||||
|   def list_ammo_groups(search, type, %{id: user_id}, show_used \\ false) do | ||||
|     from(ag in AmmoGroup, | ||||
|       as: :ag, | ||||
|       join: at in assoc(ag, :ammo_type), | ||||
|       as: :at, | ||||
| @@ -718,17 +774,32 @@ defmodule Cannery.Ammo do | ||||
|       distinct: ag.id, | ||||
|       preload: ^@ammo_group_preloads | ||||
|     ) | ||||
|     |> list_ammo_groups_include_empty(include_empty) | ||||
|     |> list_ammo_groups_filter_on_type(type) | ||||
|     |> list_ammo_groups_show_used(show_used) | ||||
|     |> list_ammo_groups_search(search) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   defp list_ammo_groups_include_empty(query, true), do: query | ||||
|   @spec list_ammo_groups_filter_on_type(Queryable.t(), AmmoType.type() | :all) :: Queryable.t() | ||||
|   defp list_ammo_groups_filter_on_type(query, :rifle), | ||||
|     do: query |> where([at: at], at.type == :rifle) | ||||
|  | ||||
|   defp list_ammo_groups_include_empty(query, false) do | ||||
|     query |> where([ag], not (ag.count == 0)) | ||||
|   defp list_ammo_groups_filter_on_type(query, :pistol), | ||||
|     do: query |> where([at: at], at.type == :pistol) | ||||
|  | ||||
|   defp list_ammo_groups_filter_on_type(query, :shotgun), | ||||
|     do: query |> where([at: at], at.type == :shotgun) | ||||
|  | ||||
|   defp list_ammo_groups_filter_on_type(query, _all), do: query | ||||
|  | ||||
|   @spec list_ammo_groups_show_used(Queryable.t(), show_used :: boolean()) :: Queryable.t() | ||||
|   defp list_ammo_groups_show_used(query, true), do: query | ||||
|  | ||||
|   defp list_ammo_groups_show_used(query, _false) do | ||||
|     query |> where([ag: ag], not (ag.count == 0)) | ||||
|   end | ||||
|  | ||||
|   @spec list_ammo_groups_show_used(Queryable.t(), search :: String.t() | nil) :: Queryable.t() | ||||
|   defp list_ammo_groups_search(query, nil), do: query | ||||
|   defp list_ammo_groups_search(query, ""), do: query | ||||
|  | ||||
|   | ||||
| @@ -42,30 +42,47 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|     field :name, :string | ||||
|     field :desc, :string | ||||
|  | ||||
|     field :type, Ecto.Enum, values: [:rifle, :shotgun, :pistol] | ||||
|  | ||||
|     # common fields | ||||
|     # https://shootersreference.com/reloadingdata/bullet_abbreviations/ | ||||
|     field :bullet_type, :string | ||||
|     field :bullet_core, :string | ||||
|     field :cartridge, :string | ||||
|     # also gauge for shotguns | ||||
|     field :caliber, :string | ||||
|     field :case_material, :string | ||||
|     field :jacket_type, :string | ||||
|     field :muzzle_velocity, :integer | ||||
|     field :powder_type, :string | ||||
|     field :powder_grains_per_charge, :integer | ||||
|     field :grains, :integer | ||||
|     field :pressure, :string | ||||
|     field :primer_type, :string | ||||
|     field :firing_type, :string | ||||
|     field :manufacturer, :string | ||||
|     field :upc, :string | ||||
|  | ||||
|     field :tracer, :boolean, default: false | ||||
|     field :incendiary, :boolean, default: false | ||||
|     field :blank, :boolean, default: false | ||||
|     field :corrosive, :boolean, default: false | ||||
|  | ||||
|     field :manufacturer, :string | ||||
|     field :upc, :string | ||||
|     # rifle/pistol fields | ||||
|     field :cartridge, :string | ||||
|     field :jacket_type, :string | ||||
|     field :powder_grains_per_charge, :integer | ||||
|     field :muzzle_velocity, :integer | ||||
|  | ||||
|     # shotgun fields | ||||
|     field :wadding, :string | ||||
|     field :shot_type, :string | ||||
|     field :shot_material, :string | ||||
|     field :shot_size, :string | ||||
|     field :unfired_length, :string | ||||
|     field :brass_height, :string | ||||
|     field :chamber_size, :string | ||||
|     field :load_grains, :integer | ||||
|     field :shot_charge_weight, :string | ||||
|     field :dram_equivalent, :string | ||||
|  | ||||
|     field :user_id, :binary_id | ||||
|  | ||||
|     has_many :ammo_groups, AmmoGroup | ||||
|  | ||||
|     timestamps() | ||||
| @@ -75,6 +92,7 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|           id: id(), | ||||
|           name: String.t(), | ||||
|           desc: String.t() | nil, | ||||
|           type: type(), | ||||
|           bullet_type: String.t() | nil, | ||||
|           bullet_core: String.t() | nil, | ||||
|           cartridge: String.t() | nil, | ||||
| @@ -88,6 +106,16 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|           pressure: String.t() | nil, | ||||
|           primer_type: String.t() | nil, | ||||
|           firing_type: String.t() | nil, | ||||
|           wadding: String.t() | nil, | ||||
|           shot_type: String.t() | nil, | ||||
|           shot_material: String.t() | nil, | ||||
|           shot_size: String.t() | nil, | ||||
|           unfired_length: String.t() | nil, | ||||
|           brass_height: String.t() | nil, | ||||
|           chamber_size: String.t() | nil, | ||||
|           load_grains: integer() | nil, | ||||
|           shot_charge_weight: String.t() | nil, | ||||
|           dram_equivalent: String.t() | nil, | ||||
|           tracer: boolean(), | ||||
|           incendiary: boolean(), | ||||
|           blank: boolean(), | ||||
| @@ -102,12 +130,14 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|   @type new_ammo_type :: %__MODULE__{} | ||||
|   @type id :: UUID.t() | ||||
|   @type changeset :: Changeset.t(t() | new_ammo_type()) | ||||
|   @type type :: :rifle | :shotgun | :pistol | nil | ||||
|  | ||||
|   @spec changeset_fields() :: [atom()] | ||||
|   defp changeset_fields, | ||||
|     do: [ | ||||
|       :name, | ||||
|       :desc, | ||||
|       :type, | ||||
|       :bullet_type, | ||||
|       :bullet_core, | ||||
|       :cartridge, | ||||
| @@ -121,6 +151,16 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|       :pressure, | ||||
|       :primer_type, | ||||
|       :firing_type, | ||||
|       :wadding, | ||||
|       :shot_type, | ||||
|       :shot_material, | ||||
|       :shot_size, | ||||
|       :unfired_length, | ||||
|       :brass_height, | ||||
|       :chamber_size, | ||||
|       :load_grains, | ||||
|       :shot_charge_weight, | ||||
|       :dram_equivalent, | ||||
|       :tracer, | ||||
|       :incendiary, | ||||
|       :blank, | ||||
| @@ -143,6 +183,15 @@ defmodule Cannery.Ammo.AmmoType do | ||||
|       :pressure, | ||||
|       :primer_type, | ||||
|       :firing_type, | ||||
|       :wadding, | ||||
|       :shot_type, | ||||
|       :shot_material, | ||||
|       :shot_size, | ||||
|       :unfired_length, | ||||
|       :brass_height, | ||||
|       :chamber_size, | ||||
|       :shot_charge_weight, | ||||
|       :dram_equivalent, | ||||
|       :manufacturer, | ||||
|       :upc | ||||
|     ] | ||||
|   | ||||
| @@ -5,6 +5,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | ||||
|   use CanneryWeb, :live_component | ||||
|   alias Cannery.{Accounts.User, Ammo.AmmoGroup, ComparableDate} | ||||
|   alias Cannery.{ActivityLog, Ammo, Containers} | ||||
|   alias CanneryWeb.Components.TableComponent | ||||
|   alias Ecto.UUID | ||||
|   alias Phoenix.LiveView.{Rendered, Socket} | ||||
|  | ||||
| @@ -54,59 +55,47 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | ||||
|          } = socket | ||||
|        ) do | ||||
|     columns = | ||||
|       if actions == [] do | ||||
|         [] | ||||
|       else | ||||
|         [%{label: gettext("Actions"), key: :actions, sortable: false}] | ||||
|       end | ||||
|  | ||||
|     columns = [ | ||||
|       %{label: gettext("Purchased on"), key: :purchased_on, type: ComparableDate}, | ||||
|       %{label: gettext("Last used on"), key: :used_up_on, type: ComparableDate} | columns | ||||
|     ] | ||||
|  | ||||
|     columns = | ||||
|       if container == [] do | ||||
|         columns | ||||
|       else | ||||
|         [%{label: gettext("Container"), key: :container} | columns] | ||||
|       end | ||||
|  | ||||
|     columns = | ||||
|       if range == [] do | ||||
|         columns | ||||
|       else | ||||
|         [%{label: gettext("Range"), key: :range} | columns] | ||||
|       end | ||||
|  | ||||
|     columns = [ | ||||
|       %{label: gettext("Price paid"), key: :price_paid}, | ||||
|       %{label: gettext("CPR"), key: :cpr} | ||||
|       | columns | ||||
|     ] | ||||
|  | ||||
|     columns = | ||||
|       if show_used do | ||||
|         [ | ||||
|           %{label: gettext("Original Count"), key: :original_count}, | ||||
|           %{label: gettext("% left"), key: :remaining} | ||||
|           | columns | ||||
|         ] | ||||
|       else | ||||
|         columns | ||||
|       end | ||||
|  | ||||
|     columns = [ | ||||
|       %{label: if(show_used, do: gettext("Current Count"), else: gettext("Count")), key: :count} | ||||
|       | columns | ||||
|     ] | ||||
|  | ||||
|     columns = | ||||
|       if ammo_type == [] do | ||||
|         columns | ||||
|       else | ||||
|         [%{label: gettext("Ammo type"), key: :ammo_type} | columns] | ||||
|       end | ||||
|       [] | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Actions"), key: :actions, sortable: false}, | ||||
|         actions != [] | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: gettext("Last used on"), | ||||
|         key: :used_up_on, | ||||
|         type: ComparableDate | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: gettext("Purchased on"), | ||||
|         key: :purchased_on, | ||||
|         type: ComparableDate | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Container"), key: :container}, | ||||
|         container != [] | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Range"), key: :range}, | ||||
|         range != [] | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("CPR"), key: :cpr}) | ||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("Price paid"), key: :price_paid}) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("% left"), key: :remaining}, | ||||
|         show_used | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Original Count"), key: :original_count}, | ||||
|         show_used | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: if(show_used, do: gettext("Current Count"), else: gettext("Count")), | ||||
|         key: :count | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Ammo type"), key: :ammo_type}, | ||||
|         ammo_type != [] | ||||
|       ) | ||||
|  | ||||
|     containers = | ||||
|       ammo_groups | ||||
| @@ -140,12 +129,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div id={@id} class="w-full"> | ||||
|       <.live_component | ||||
|         module={CanneryWeb.Components.TableComponent} | ||||
|         id={"table-#{@id}"} | ||||
|         columns={@columns} | ||||
|         rows={@rows} | ||||
|       /> | ||||
|       <.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} /> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
|   | ||||
| @@ -4,6 +4,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|   """ | ||||
|   use CanneryWeb, :live_component | ||||
|   alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoType} | ||||
|   alias CanneryWeb.Components.TableComponent | ||||
|   alias Ecto.UUID | ||||
|   alias Phoenix.LiveView.{Rendered, Socket} | ||||
|  | ||||
| @@ -12,6 +13,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|           %{ | ||||
|             required(:id) => UUID.t(), | ||||
|             required(:current_user) => User.t(), | ||||
|             optional(:type) => AmmoType.type() | nil, | ||||
|             optional(:show_used) => boolean(), | ||||
|             optional(:ammo_types) => [AmmoType.t()], | ||||
|             optional(:actions) => Rendered.t(), | ||||
| @@ -24,6 +26,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|       socket | ||||
|       |> assign(assigns) | ||||
|       |> assign_new(:show_used, fn -> false end) | ||||
|       |> assign_new(:type, fn -> :all end) | ||||
|       |> assign_new(:actions, fn -> [] end) | ||||
|       |> display_ammo_types() | ||||
|  | ||||
| @@ -36,90 +39,118 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|              ammo_types: ammo_types, | ||||
|              current_user: current_user, | ||||
|              show_used: show_used, | ||||
|              type: type, | ||||
|              actions: actions | ||||
|            } | ||||
|          } = socket | ||||
|        ) do | ||||
|     columns = | ||||
|     filtered_columns = | ||||
|       [ | ||||
|         %{label: gettext("Name"), key: :name, type: :name}, | ||||
|         %{label: gettext("Bullet type"), key: :bullet_type, type: :string}, | ||||
|         %{label: gettext("Bullet core"), key: :bullet_core, type: :string}, | ||||
|         %{label: gettext("Cartridge"), key: :cartridge, type: :string}, | ||||
|         %{label: gettext("Caliber"), key: :caliber, type: :string}, | ||||
|         %{label: gettext("Case material"), key: :case_material, type: :string}, | ||||
|         %{ | ||||
|           label: if(type == :shotgun, do: gettext("Gauge"), else: gettext("Caliber")), | ||||
|           key: :caliber, | ||||
|           type: :string | ||||
|         }, | ||||
|         %{label: gettext("Unfired shell length"), key: :unfired_length, type: :string}, | ||||
|         %{label: gettext("Brass height"), key: :brass_height, type: :string}, | ||||
|         %{label: gettext("Chamber size"), key: :chamber_size, type: :string}, | ||||
|         %{label: gettext("Chamber size"), key: :chamber_size, type: :string}, | ||||
|         %{label: gettext("Grains"), key: :grains, type: :string}, | ||||
|         %{label: gettext("Bullet type"), key: :bullet_type, type: :string}, | ||||
|         %{ | ||||
|           label: if(type == :shotgun, do: gettext("Slug core"), else: gettext("Bullet core")), | ||||
|           key: :bullet_core, | ||||
|           type: :string | ||||
|         }, | ||||
|         %{label: gettext("Jacket type"), key: :jacket_type, type: :string}, | ||||
|         %{label: gettext("Muzzle velocity"), key: :muzzle_velocity, type: :string}, | ||||
|         %{label: gettext("Case material"), key: :case_material, type: :string}, | ||||
|         %{label: gettext("Wadding"), key: :wadding, type: :string}, | ||||
|         %{label: gettext("Shot type"), key: :shot_type, type: :string}, | ||||
|         %{label: gettext("Shot material"), key: :shot_material, type: :string}, | ||||
|         %{label: gettext("Shot size"), key: :shot_size, type: :string}, | ||||
|         %{label: gettext("Load grains"), key: :load_grains, type: :string}, | ||||
|         %{label: gettext("Shot charge weight"), key: :shot_charge_weight, type: :string}, | ||||
|         %{label: gettext("Powder type"), key: :powder_type, type: :string}, | ||||
|         %{ | ||||
|           label: gettext("Powder grains per charge"), | ||||
|           key: :powder_grains_per_charge, | ||||
|           type: :string | ||||
|         }, | ||||
|         %{label: gettext("Grains"), key: :grains, type: :string}, | ||||
|         %{label: gettext("Pressure"), key: :pressure, type: :string}, | ||||
|         %{label: gettext("Dram equivalent"), key: :dram_equivalent, type: :string}, | ||||
|         %{label: gettext("Muzzle velocity"), key: :muzzle_velocity, type: :string}, | ||||
|         %{label: gettext("Primer type"), key: :primer_type, type: :string}, | ||||
|         %{label: gettext("Firing type"), key: :firing_type, type: :string}, | ||||
|         %{label: gettext("Tracer"), key: :tracer, type: :boolean}, | ||||
|         %{label: gettext("Incendiary"), key: :incendiary, type: :boolean}, | ||||
|         %{label: gettext("Blank"), key: :blank, type: :boolean}, | ||||
|         %{label: gettext("Corrosive"), key: :corrosive, type: :boolean}, | ||||
|         %{label: gettext("Manufacturer"), key: :manufacturer, type: :string}, | ||||
|         %{label: gettext("UPC"), key: "upc", type: :string} | ||||
|         %{label: gettext("Tracer"), key: :tracer, type: :atom}, | ||||
|         %{label: gettext("Incendiary"), key: :incendiary, type: :atom}, | ||||
|         %{label: gettext("Blank"), key: :blank, type: :atom}, | ||||
|         %{label: gettext("Corrosive"), key: :corrosive, type: :atom}, | ||||
|         %{label: gettext("Manufacturer"), key: :manufacturer, type: :string} | ||||
|       ] | ||||
|       |> Enum.filter(fn %{key: key, type: type} -> | ||||
|         # remove columns if all values match defaults | ||||
|         default_value = if type == :boolean, do: false, else: nil | ||||
|         default_value = if type == :atom, do: false, else: nil | ||||
|  | ||||
|         ammo_types | ||||
|         |> Enum.any?(fn ammo_type -> | ||||
|           not (ammo_type |> Map.get(key) == default_value) | ||||
|         end) | ||||
|         |> Enum.any?(fn ammo_type -> Map.get(ammo_type, key, default_value) != default_value end) | ||||
|       end) | ||||
|       |> Kernel.++([ | ||||
|         %{label: gettext("Rounds"), key: :round_count, type: :round_count} | ||||
|       ]) | ||||
|       |> Kernel.++( | ||||
|         if show_used do | ||||
|           [ | ||||
|             %{ | ||||
|               label: gettext("Used rounds"), | ||||
|               key: :used_round_count, | ||||
|               type: :used_round_count | ||||
|             }, | ||||
|             %{ | ||||
|               label: gettext("Total ever rounds"), | ||||
|               key: :historical_round_count, | ||||
|               type: :historical_round_count | ||||
|             } | ||||
|           ] | ||||
|         else | ||||
|           [] | ||||
|         end | ||||
|  | ||||
|     columns = | ||||
|       [%{label: gettext("Actions"), key: "actions", type: :actions, sortable: false}] | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: gettext("Average CPR"), | ||||
|         key: :avg_price_paid, | ||||
|         type: :avg_price_paid | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{ | ||||
|           label: gettext("Total ever packs"), | ||||
|           key: :historical_pack_count, | ||||
|           type: :historical_pack_count | ||||
|         }, | ||||
|         show_used | ||||
|       ) | ||||
|       |> Kernel.++([%{label: gettext("Packs"), key: :ammo_count, type: :ammo_count}]) | ||||
|       |> Kernel.++( | ||||
|         if show_used do | ||||
|           [ | ||||
|             %{ | ||||
|               label: gettext("Used packs"), | ||||
|               key: :used_pack_count, | ||||
|               type: :used_pack_count | ||||
|             }, | ||||
|             %{ | ||||
|               label: gettext("Total ever packs"), | ||||
|               key: :historical_pack_count, | ||||
|               type: :historical_pack_count | ||||
|             } | ||||
|           ] | ||||
|         else | ||||
|           [] | ||||
|         end | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{ | ||||
|           label: gettext("Used packs"), | ||||
|           key: :used_pack_count, | ||||
|           type: :used_pack_count | ||||
|         }, | ||||
|         show_used | ||||
|       ) | ||||
|       |> Kernel.++([ | ||||
|         %{label: gettext("Average CPR"), key: :avg_price_paid, type: :avg_price_paid}, | ||||
|         %{label: gettext("Actions"), key: "actions", type: :actions, sortable: false} | ||||
|       ]) | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: gettext("Packs"), | ||||
|         key: :ammo_count, | ||||
|         type: :ammo_count | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{ | ||||
|           label: gettext("Total ever rounds"), | ||||
|           key: :historical_round_count, | ||||
|           type: :historical_round_count | ||||
|         }, | ||||
|         show_used | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{ | ||||
|           label: gettext("Used rounds"), | ||||
|           key: :used_round_count, | ||||
|           type: :used_round_count | ||||
|         }, | ||||
|         show_used | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns(%{ | ||||
|         label: gettext("Rounds"), | ||||
|         key: :round_count, | ||||
|         type: :round_count | ||||
|       }) | ||||
|       |> TableComponent.maybe_compose_columns(filtered_columns) | ||||
|       |> TableComponent.maybe_compose_columns( | ||||
|         %{label: gettext("Type"), key: :type, type: :atom}, | ||||
|         type in [:all, nil] | ||||
|       ) | ||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("Name"), key: :name, type: :name}) | ||||
|  | ||||
|     round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user) | ||||
|     packs_count = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user) | ||||
| @@ -162,12 +193,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div id={@id} class="w-full"> | ||||
|       <.live_component | ||||
|         module={CanneryWeb.Components.TableComponent} | ||||
|         id={"table-#{@id}"} | ||||
|         columns={@columns} | ||||
|         rows={@rows} | ||||
|       /> | ||||
|       <.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} /> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| @@ -179,7 +205,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   defp get_ammo_type_value(:boolean, key, ammo_type, _other_data), | ||||
|   defp get_ammo_type_value(:atom, key, ammo_type, _other_data), | ||||
|     do: ammo_type |> Map.get(key) |> humanize() | ||||
|  | ||||
|   defp get_ammo_type_value(:round_count, _key, %{id: ammo_type_id}, %{round_counts: round_counts}), | ||||
|   | ||||
| @@ -3,7 +3,7 @@ defmodule CanneryWeb.ExportController do | ||||
|   alias Cannery.{ActivityLog, Ammo, Containers} | ||||
|  | ||||
|   def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do | ||||
|     ammo_types = Ammo.list_ammo_types(current_user) | ||||
|     ammo_types = Ammo.list_ammo_types(current_user, :all) | ||||
|     used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user) | ||||
|     round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user) | ||||
|     ammo_group_counts = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user) | ||||
| @@ -28,7 +28,7 @@ defmodule CanneryWeb.ExportController do | ||||
|         }) | ||||
|       end) | ||||
|  | ||||
|     ammo_groups = Ammo.list_ammo_groups(nil, true, current_user) | ||||
|     ammo_groups = Ammo.list_ammo_groups(nil, :all, current_user, true) | ||||
|     used_counts = ammo_groups |> ActivityLog.get_used_counts(current_user) | ||||
|     original_counts = ammo_groups |> Ammo.get_original_counts(current_user) | ||||
|     cprs = ammo_groups |> Ammo.get_cprs(current_user) | ||||
| @@ -48,7 +48,7 @@ defmodule CanneryWeb.ExportController do | ||||
|         }) | ||||
|       end) | ||||
|  | ||||
|     shot_groups = ActivityLog.list_shot_groups(current_user) | ||||
|     shot_groups = ActivityLog.list_shot_groups(:all, current_user) | ||||
|  | ||||
|     containers = | ||||
|       Containers.list_containers(current_user) | ||||
|   | ||||
| @@ -26,7 +26,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | ||||
|       socket = | ||||
|       socket | ||||
|       |> assign(:ammo_group_create_limit, @ammo_group_create_limit) | ||||
|       |> assign(:ammo_types, Ammo.list_ammo_types(current_user)) | ||||
|       |> assign(:ammo_types, Ammo.list_ammo_types(current_user, :all)) | ||||
|       |> assign_new(:containers, fn -> Containers.list_containers(current_user) end) | ||||
|  | ||||
|     params = | ||||
|   | ||||
| @@ -8,11 +8,11 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | ||||
|  | ||||
|   @impl true | ||||
|   def mount(%{"search" => search}, _session, socket) do | ||||
|     {:ok, socket |> assign(show_used: false, search: search) |> display_ammo_groups()} | ||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: search) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket |> assign(show_used: false, search: nil) |> display_ammo_groups()} | ||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: nil) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
| @@ -119,10 +119,36 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :rifle) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :shotgun) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :pistol) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :all) |> display_ammo_groups()} | ||||
|   end | ||||
|  | ||||
|   defp display_ammo_groups( | ||||
|          %{assigns: %{search: search, current_user: current_user, show_used: show_used}} = socket | ||||
|          %{ | ||||
|            assigns: %{ | ||||
|              type: type, | ||||
|              search: search, | ||||
|              current_user: current_user, | ||||
|              show_used: show_used | ||||
|            } | ||||
|          } = socket | ||||
|        ) do | ||||
|     ammo_groups = Ammo.list_ammo_groups(search, show_used, current_user) | ||||
|     # get total number of ammo groups to determine whether to display onboarding | ||||
|     # prompts | ||||
|     ammo_groups_count = Ammo.get_ammo_groups_count!(current_user, true) | ||||
|     ammo_groups = Ammo.list_ammo_groups(search, type, current_user, show_used) | ||||
|     ammo_types_count = Ammo.get_ammo_types_count!(current_user) | ||||
|     containers_count = Containers.get_containers_count!(current_user) | ||||
|  | ||||
| @@ -130,7 +156,8 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | ||||
|     |> assign( | ||||
|       ammo_groups: ammo_groups, | ||||
|       ammo_types_count: ammo_types_count, | ||||
|       containers_count: containers_count | ||||
|       containers_count: containers_count, | ||||
|       ammo_groups_count: ammo_groups_count | ||||
|     ) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,14 +3,6 @@ | ||||
|     <%= gettext("Ammo") %> | ||||
|   </h1> | ||||
|  | ||||
|   <h2 | ||||
|     :if={@ammo_groups |> Enum.empty?() and @search |> is_nil()} | ||||
|     class="title text-xl text-primary-600" | ||||
|   > | ||||
|     <%= gettext("No Ammo") %> | ||||
|     <%= display_emoji("😔") %> | ||||
|   </h2> | ||||
|  | ||||
|   <%= cond do %> | ||||
|     <% @containers_count == 0 -> %> | ||||
|       <div class="flex justify-center items-center"> | ||||
| @@ -32,7 +24,12 @@ | ||||
|           <%= dgettext("actions", "add an ammo type first") %> | ||||
|         </.link> | ||||
|       </div> | ||||
|     <% @ammo_groups |> Enum.empty?() and @search |> is_nil() -> %> | ||||
|     <% @ammo_groups_count == 0 -> %> | ||||
|       <h2 class="title text-xl text-primary-600"> | ||||
|         <%= gettext("No ammo") %> | ||||
|         <%= display_emoji("😔") %> | ||||
|       </h2> | ||||
|  | ||||
|       <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary"> | ||||
|         <%= dgettext("actions", "Add your first box!") %> | ||||
|       </.link> | ||||
| @@ -40,144 +37,168 @@ | ||||
|       <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary"> | ||||
|         <%= dgettext("actions", "Add Ammo") %> | ||||
|       </.link> | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl"> | ||||
|     <.form | ||||
|       :let={f} | ||||
|       for={%{}} | ||||
|       as={:search} | ||||
|       phx-change="search" | ||||
|       phx-submit="search" | ||||
|       class="grow self-stretch flex flex-col items-stretch" | ||||
|     > | ||||
|       <%= text_input(f, :search_term, | ||||
|         class: "input input-primary", | ||||
|         value: @search, | ||||
|         role: "search", | ||||
|         phx_debounce: 300, | ||||
|         placeholder: gettext("Search ammo") | ||||
|       ) %> | ||||
|     </.form> | ||||
|       <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl"> | ||||
|         <.form | ||||
|           :let={f} | ||||
|           for={%{}} | ||||
|           as={:ammo_type} | ||||
|           phx-change="change_type" | ||||
|           phx-submit="change_type" | ||||
|           class="flex items-center" | ||||
|         > | ||||
|           <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> | ||||
|  | ||||
|     <.toggle_button action="toggle_show_used" value={@show_used}> | ||||
|       <span class="title text-lg text-primary-600"> | ||||
|         <%= gettext("Show used") %> | ||||
|       </span> | ||||
|     </.toggle_button> | ||||
|   </div> | ||||
|           <%= select( | ||||
|             f, | ||||
|             :type, | ||||
|             [ | ||||
|               {gettext("All"), :all}, | ||||
|               {gettext("Rifle"), :rifle}, | ||||
|               {gettext("Shotgun"), :shotgun}, | ||||
|               {gettext("Pistol"), :pistol} | ||||
|             ], | ||||
|             class: "mx-2 my-1 min-w-md input input-primary", | ||||
|             value: @type | ||||
|           ) %> | ||||
|         </.form> | ||||
|  | ||||
|   <%= if @ammo_groups |> Enum.empty?() do %> | ||||
|     <h2 class="title text-xl text-primary-600"> | ||||
|       <%= gettext("No Ammo") %> | ||||
|       <%= display_emoji("😔") %> | ||||
|     </h2> | ||||
|   <% else %> | ||||
|     <.live_component | ||||
|       module={CanneryWeb.Components.AmmoGroupTableComponent} | ||||
|       id="ammo-group-index-table" | ||||
|       ammo_groups={@ammo_groups} | ||||
|       current_user={@current_user} | ||||
|       show_used={@show_used} | ||||
|     > | ||||
|       <:ammo_type :let={%{name: ammo_type_name} = ammo_type}> | ||||
|         <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link"> | ||||
|           <%= ammo_type_name %> | ||||
|         </.link> | ||||
|       </:ammo_type> | ||||
|       <:range :let={ammo_group}> | ||||
|         <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||
|           <button | ||||
|             type="button" | ||||
|             class="mx-2 my-1 text-sm btn btn-primary" | ||||
|             phx-click="toggle_staged" | ||||
|             phx-value-ammo_group_id={ammo_group.id} | ||||
|           > | ||||
|             <%= if ammo_group.staged, | ||||
|               do: dgettext("actions", "Unstage"), | ||||
|               else: dgettext("actions", "Stage") %> | ||||
|           </button> | ||||
|         <.form | ||||
|           :let={f} | ||||
|           for={%{}} | ||||
|           as={:search} | ||||
|           phx-change="search" | ||||
|           phx-submit="search" | ||||
|           class="grow flex items-center" | ||||
|         > | ||||
|           <%= text_input(f, :search_term, | ||||
|             class: "grow input input-primary", | ||||
|             value: @search, | ||||
|             role: "search", | ||||
|             phx_debounce: 300, | ||||
|             placeholder: gettext("Search ammo") | ||||
|           ) %> | ||||
|         </.form> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group)} | ||||
|             class="mx-2 my-1 text-sm btn btn-primary" | ||||
|           > | ||||
|             <%= dgettext("actions", "Record shots") %> | ||||
|           </.link> | ||||
|         </div> | ||||
|       </:range> | ||||
|       <:container :let={{ammo_group, %{name: container_name} = container}}> | ||||
|         <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||
|           <.link | ||||
|             navigate={Routes.container_show_path(Endpoint, :show, container)} | ||||
|             class="mx-2 my-1 link" | ||||
|           > | ||||
|             <%= container_name %> | ||||
|           </.link> | ||||
|         <.toggle_button action="toggle_show_used" value={@show_used}> | ||||
|           <span class="title text-lg text-primary-600"> | ||||
|             <%= gettext("Show used") %> | ||||
|           </span> | ||||
|         </.toggle_button> | ||||
|       </div> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)} | ||||
|             class="mx-2 my-1 text-sm btn btn-primary" | ||||
|           > | ||||
|             <%= dgettext("actions", "Move ammo") %> | ||||
|           </.link> | ||||
|         </div> | ||||
|       </:container> | ||||
|       <:actions :let={%{count: ammo_group_count} = ammo_group}> | ||||
|         <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center"> | ||||
|           <.link | ||||
|             navigate={Routes.ammo_group_show_path(Endpoint, :show, ammo_group)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "View ammo group of %{ammo_group_count} bullets", | ||||
|                 ammo_group_count: ammo_group_count | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-eye"></i> | ||||
|           </.link> | ||||
|       <%= if @ammo_groups |> Enum.empty?() do %> | ||||
|         <h2 class="title text-xl text-primary-600"> | ||||
|           <%= gettext("No Ammo") %> | ||||
|           <%= display_emoji("😔") %> | ||||
|         </h2> | ||||
|       <% else %> | ||||
|         <.live_component | ||||
|           module={CanneryWeb.Components.AmmoGroupTableComponent} | ||||
|           id="ammo-group-index-table" | ||||
|           ammo_groups={@ammo_groups} | ||||
|           current_user={@current_user} | ||||
|           show_used={@show_used} | ||||
|         > | ||||
|           <:ammo_type :let={%{name: ammo_type_name} = ammo_type}> | ||||
|             <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link"> | ||||
|               <%= ammo_type_name %> | ||||
|             </.link> | ||||
|           </:ammo_type> | ||||
|           <:range :let={ammo_group}> | ||||
|             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||
|               <button | ||||
|                 type="button" | ||||
|                 class="mx-2 my-1 text-sm btn btn-primary" | ||||
|                 phx-click="toggle_staged" | ||||
|                 phx-value-ammo_group_id={ammo_group.id} | ||||
|               > | ||||
|                 <%= if ammo_group.staged, | ||||
|                   do: dgettext("actions", "Unstage"), | ||||
|                   else: dgettext("actions", "Stage") %> | ||||
|               </button> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Edit ammo group of %{ammo_group_count} bullets", | ||||
|                 ammo_group_count: ammo_group_count | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|           </.link> | ||||
|               <.link | ||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group)} | ||||
|                 class="mx-2 my-1 text-sm btn btn-primary" | ||||
|               > | ||||
|                 <%= dgettext("actions", "Record shots") %> | ||||
|               </.link> | ||||
|             </div> | ||||
|           </:range> | ||||
|           <:container :let={{ammo_group, %{name: container_name} = container}}> | ||||
|             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||
|               <.link | ||||
|                 navigate={Routes.container_show_path(Endpoint, :show, container)} | ||||
|                 class="mx-2 my-1 link" | ||||
|               > | ||||
|                 <%= container_name %> | ||||
|               </.link> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Clone ammo group of %{ammo_group_count} bullets", | ||||
|                 ammo_group_count: ammo_group_count | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|           </.link> | ||||
|               <.link | ||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)} | ||||
|                 class="mx-2 my-1 text-sm btn btn-primary" | ||||
|               > | ||||
|                 <%= dgettext("actions", "Move ammo") %> | ||||
|               </.link> | ||||
|             </div> | ||||
|           </:container> | ||||
|           <:actions :let={%{count: ammo_group_count} = ammo_group}> | ||||
|             <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center"> | ||||
|               <.link | ||||
|                 navigate={Routes.ammo_group_show_path(Endpoint, :show, ammo_group)} | ||||
|                 class="text-primary-600 link" | ||||
|                 aria-label={ | ||||
|                   dgettext("actions", "View ammo group of %{ammo_group_count} bullets", | ||||
|                     ammo_group_count: ammo_group_count | ||||
|                   ) | ||||
|                 } | ||||
|               > | ||||
|                 <i class="fa-fw fa-lg fas fa-eye"></i> | ||||
|               </.link> | ||||
|  | ||||
|           <.link | ||||
|             href="#" | ||||
|             class="text-primary-600 link" | ||||
|             phx-click="delete" | ||||
|             phx-value-id={ammo_group.id} | ||||
|             data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")} | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets", | ||||
|                 ammo_group_count: ammo_group_count | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           </.link> | ||||
|         </div> | ||||
|       </:actions> | ||||
|     </.live_component> | ||||
|               <.link | ||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)} | ||||
|                 class="text-primary-600 link" | ||||
|                 aria-label={ | ||||
|                   dgettext("actions", "Edit ammo group of %{ammo_group_count} bullets", | ||||
|                     ammo_group_count: ammo_group_count | ||||
|                   ) | ||||
|                 } | ||||
|               > | ||||
|                 <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|               </.link> | ||||
|  | ||||
|               <.link | ||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)} | ||||
|                 class="text-primary-600 link" | ||||
|                 aria-label={ | ||||
|                   dgettext("actions", "Clone ammo group of %{ammo_group_count} bullets", | ||||
|                     ammo_group_count: ammo_group_count | ||||
|                   ) | ||||
|                 } | ||||
|               > | ||||
|                 <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|               </.link> | ||||
|  | ||||
|               <.link | ||||
|                 href="#" | ||||
|                 class="text-primary-600 link" | ||||
|                 phx-click="delete" | ||||
|                 phx-value-id={ammo_group.id} | ||||
|                 data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")} | ||||
|                 aria-label={ | ||||
|                   dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets", | ||||
|                     ammo_group_count: ammo_group_count | ||||
|                   ) | ||||
|                 } | ||||
|               > | ||||
|                 <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|               </.link> | ||||
|             </div> | ||||
|           </:actions> | ||||
|         </.live_component> | ||||
|       <% end %> | ||||
|   <% end %> | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -15,9 +15,19 @@ | ||||
|       :if={@changeset.action && not @changeset.valid?()} | ||||
|       class="invalid-feedback col-span-3 text-center" | ||||
|     > | ||||
|       <%= changeset_errors(@changeset) %> | ||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|     </div> | ||||
|  | ||||
|     <%= label(f, :type, gettext("Type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= select( | ||||
|       f, | ||||
|       :type, | ||||
|       [{gettext("Rifle"), :rifle}, {gettext("Shotgun"), :shotgun}, {gettext("Pistol"), :pistol}], | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255 | ||||
|     ) %> | ||||
|     <%= error_tag(f, :type, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :name, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
| @@ -34,37 +44,31 @@ | ||||
|     ) %> | ||||
|     <%= error_tag(f, :desc, "col-span-3 text-center") %> | ||||
|  | ||||
|     <.link | ||||
|       href="https://shootersreference.com/reloadingdata/bullet_abbreviations/" | ||||
|       class="col-span-3 text-center link title text-md text-primary-600" | ||||
|     > | ||||
|       <%= gettext("Example bullet type abbreviations") %> | ||||
|     </.link> | ||||
|     <%= label(f, :bullet_type, gettext("Bullet type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :bullet_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("FMJ") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :bullet_type, "col-span-3 text-center") %> | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Dimensions") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :bullet_core, gettext("Bullet core"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :bullet_core, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("Steel") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :bullet_core, "col-span-3 text-center") %> | ||||
|     <%= if Changeset.get_field(@changeset, :type) in [:rifle, :pistol] do %> | ||||
|       <%= label(f, :cartridge, gettext("Cartridge"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :cartridge, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255, | ||||
|         placeholder: gettext("5.56x46mm NATO") | ||||
|       ) %> | ||||
|       <%= error_tag(f, :cartridge, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :cartridge, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :cartridge, gettext("Cartridge"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :cartridge, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("5.56x46mm NATO") | ||||
|     <%= label( | ||||
|       f, | ||||
|       :caliber, | ||||
|       if(Changeset.get_field(@changeset, :type) == :shotgun, | ||||
|         do: gettext("Gauge"), | ||||
|         else: gettext("Caliber") | ||||
|       ), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :cartridge, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :caliber, gettext("Caliber"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :caliber, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
| @@ -72,48 +76,38 @@ | ||||
|     ) %> | ||||
|     <%= error_tag(f, :caliber, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :case_material, gettext("Case material"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :case_material, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("Brass") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :case_material, "col-span-3 text-center") %> | ||||
|     <%= if Changeset.get_field(@changeset, :type) == :shotgun do %> | ||||
|       <%= label(f, :unfired_length, gettext("Unfired shell length"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= text_input(f, :unfired_length, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :unfired_length, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :jacket_type, gettext("Jacket type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :jacket_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("Bimetal") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :case_material, "col-span-3 text-center") %> | ||||
|       <%= label(f, :brass_height, gettext("Brass height"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :brass_height, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :brass_height, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :muzzle_velocity, gettext("Muzzle velocity"), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= number_input(f, :muzzle_velocity, | ||||
|       step: "1", | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       min: 1 | ||||
|     ) %> | ||||
|     <%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %> | ||||
|       <%= label(f, :chamber_size, gettext("Chamber size"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :chamber_size, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :chamber_size, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :unfired_length, value: nil) %> | ||||
|       <%= hidden_input(f, :brass_height, value: nil) %> | ||||
|       <%= hidden_input(f, :chamber_size, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :powder_type, gettext("Powder type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :powder_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255 | ||||
|     ) %> | ||||
|     <%= error_tag(f, :powder_type, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label(f, :powder_grains_per_charge, gettext("Powder grains per charge"), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= number_input(f, :powder_grains_per_charge, | ||||
|       step: "1", | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       min: 1 | ||||
|     ) %> | ||||
|     <%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %> | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Projectile") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :grains, gettext("Grains"), class: "title text-lg text-primary-600") %> | ||||
|     <%= number_input(f, :grains, | ||||
| @@ -123,6 +117,143 @@ | ||||
|     ) %> | ||||
|     <%= error_tag(f, :grains, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label f, :bullet_type, class: "flex title text-lg text-primary-600 space-x-2" do %> | ||||
|       <p><%= gettext("Bullet type") %></p> | ||||
|  | ||||
|       <.link | ||||
|         href="https://shootersreference.com/reloadingdata/bullet_abbreviations/" | ||||
|         class="link" | ||||
|         target="_blank" | ||||
|         rel="noopener noreferrer" | ||||
|       > | ||||
|         <i class="fas fa-md fa-external-link-alt"></i> | ||||
|       </.link> | ||||
|     <% end %> | ||||
|     <%= text_input(f, :bullet_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("FMJ") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :bullet_type, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= label( | ||||
|       f, | ||||
|       :bullet_core, | ||||
|       if(Changeset.get_field(@changeset, :type) == :shotgun, | ||||
|         do: gettext("Slug core"), | ||||
|         else: gettext("Bullet core") | ||||
|       ), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= text_input(f, :bullet_core, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("Steel") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :bullet_core, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= if Changeset.get_field(@changeset, :type) in [:rifle, :pistol] do %> | ||||
|       <%= label(f, :jacket_type, gettext("Jacket type"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :jacket_type, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255, | ||||
|         placeholder: gettext("Bimetal") | ||||
|       ) %> | ||||
|       <%= error_tag(f, :jacket_type, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :jacket_type, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :case_material, gettext("Case material"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :case_material, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255, | ||||
|       placeholder: gettext("Brass") | ||||
|     ) %> | ||||
|     <%= error_tag(f, :case_material, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= if Changeset.get_field(@changeset, :type) == :shotgun do %> | ||||
|       <%= label(f, :wadding, gettext("Wadding"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :wadding, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :wadding, "col-span-3 text-center") %> | ||||
|  | ||||
|       <%= label(f, :shot_type, gettext("Shot type"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :shot_type, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255, | ||||
|         placeholder: gettext("Target, bird, buck, etc") | ||||
|       ) %> | ||||
|       <%= error_tag(f, :shot_type, "col-span-3 text-center") %> | ||||
|  | ||||
|       <%= label(f, :shot_material, gettext("Shot material"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= text_input(f, :shot_material, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :shot_material, "col-span-3 text-center") %> | ||||
|  | ||||
|       <%= label(f, :shot_size, gettext("Shot size"), class: "title text-lg text-primary-600") %> | ||||
|       <%= text_input(f, :shot_size, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :shot_size, "col-span-3 text-center") %> | ||||
|  | ||||
|       <%= label(f, :load_grains, gettext("Load grains"), class: "title text-lg text-primary-600") %> | ||||
|       <%= number_input(f, :load_grains, | ||||
|         step: "1", | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         min: 1 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :load_grains, "col-span-3 text-center") %> | ||||
|  | ||||
|       <%= label(f, :shot_charge_weight, gettext("Shot charge weight"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= text_input(f, :shot_charge_weight, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :shot_charge_weight, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :wadding, value: nil) %> | ||||
|       <%= hidden_input(f, :shot_type, value: nil) %> | ||||
|       <%= hidden_input(f, :shot_material, value: nil) %> | ||||
|       <%= hidden_input(f, :shot_size, value: nil) %> | ||||
|       <%= hidden_input(f, :load_grains, value: nil) %> | ||||
|       <%= hidden_input(f, :shot_charge_weight, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Powder") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :powder_type, gettext("Powder type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :powder_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|       maxlength: 255 | ||||
|     ) %> | ||||
|     <%= error_tag(f, :powder_type, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= if Changeset.get_field(@changeset, :type) in [:rifle, :pistol] do %> | ||||
|       <%= label(f, :powder_grains_per_charge, gettext("Powder grains per charge"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= number_input(f, :powder_grains_per_charge, | ||||
|         step: "1", | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         min: 1 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :powder_grains_per_charge, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :pressure, gettext("Pressure"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :pressure, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
| @@ -131,6 +262,37 @@ | ||||
|     ) %> | ||||
|     <%= error_tag(f, :pressure, "col-span-3 text-center") %> | ||||
|  | ||||
|     <%= if Changeset.get_field(@changeset, :type) == :shotgun do %> | ||||
|       <%= label(f, :dram_equivalent, gettext("Dram equivalent"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= text_input(f, :dram_equivalent, | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         maxlength: 255 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :dram_equivalent, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :dram_equivalent, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= if Changeset.get_field(@changeset, :type) in [:rifle, :pistol] do %> | ||||
|       <%= label(f, :muzzle_velocity, gettext("Muzzle velocity"), | ||||
|         class: "title text-lg text-primary-600" | ||||
|       ) %> | ||||
|       <%= number_input(f, :muzzle_velocity, | ||||
|         step: "1", | ||||
|         class: "text-center col-span-2 input input-primary", | ||||
|         min: 1 | ||||
|       ) %> | ||||
|       <%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %> | ||||
|     <% else %> | ||||
|       <%= hidden_input(f, :muzzle_velocity, value: nil) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Primer") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :primer_type, gettext("Primer type"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :primer_type, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
| @@ -147,6 +309,10 @@ | ||||
|     ) %> | ||||
|     <%= error_tag(f, :firing_type, "col-span-3 text-center") %> | ||||
|  | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Attributes") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :tracer, gettext("Tracer"), class: "title text-lg text-primary-600") %> | ||||
|     <%= checkbox(f, :tracer, class: "text-center col-span-2 checkbox") %> | ||||
|     <%= error_tag(f, :tracer, "col-span-3 text-center") %> | ||||
| @@ -163,6 +329,10 @@ | ||||
|     <%= checkbox(f, :corrosive, class: "text-center col-span-2 checkbox") %> | ||||
|     <%= error_tag(f, :corrosive, "col-span-3 text-center") %> | ||||
|  | ||||
|     <h2 class="text-center title text-lg text-primary-600 col-span-3"> | ||||
|       <%= gettext("Manufacturer") %> | ||||
|     </h2> | ||||
|  | ||||
|     <%= label(f, :manufacturer, gettext("Manufacturer"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :manufacturer, | ||||
|       class: "text-center col-span-2 input input-primary", | ||||
|   | ||||
| @@ -8,11 +8,11 @@ defmodule CanneryWeb.AmmoTypeLive.Index do | ||||
|  | ||||
|   @impl true | ||||
|   def mount(%{"search" => search}, _session, socket) do | ||||
|     {:ok, socket |> assign(show_used: false, search: search) |> list_ammo_types()} | ||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: search) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket |> assign(show_used: false, search: nil) |> list_ammo_types()} | ||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: nil) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
| @@ -86,7 +86,29 @@ defmodule CanneryWeb.AmmoTypeLive.Index do | ||||
|     {:noreply, socket |> push_patch(to: search_path)} | ||||
|   end | ||||
|  | ||||
|   defp list_ammo_types(%{assigns: %{search: search, current_user: current_user}} = socket) do | ||||
|     socket |> assign(ammo_types: Ammo.list_ammo_types(search, current_user)) | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :rifle) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :shotgun) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :pistol) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :all) |> list_ammo_types()} | ||||
|   end | ||||
|  | ||||
|   defp list_ammo_types( | ||||
|          %{assigns: %{type: type, search: search, current_user: current_user}} = socket | ||||
|        ) do | ||||
|     socket | ||||
|     |> assign( | ||||
|       ammo_types: Ammo.list_ammo_types(search, current_user, type), | ||||
|       ammo_types_count: Ammo.get_ammo_types_count!(current_user) | ||||
|     ) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     <%= gettext("Catalog") %> | ||||
|   </h1> | ||||
|  | ||||
|   <%= if @ammo_types |> Enum.empty?() and @search |> is_nil() do %> | ||||
|   <%= if @ammo_types_count == 0 do %> | ||||
|     <h2 class="title text-xl text-primary-600"> | ||||
|       <%= gettext("No Ammo types") %> | ||||
|       <%= display_emoji("😔") %> | ||||
| @@ -17,17 +17,41 @@ | ||||
|       <%= dgettext("actions", "New Ammo type") %> | ||||
|     </.link> | ||||
|  | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl"> | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl"> | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:ammo_type} | ||||
|         phx-change="change_type" | ||||
|         phx-submit="change_type" | ||||
|         class="flex items-center" | ||||
|       > | ||||
|         <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> | ||||
|  | ||||
|         <%= select( | ||||
|           f, | ||||
|           :type, | ||||
|           [ | ||||
|             {gettext("All"), :all}, | ||||
|             {gettext("Rifle"), :rifle}, | ||||
|             {gettext("Shotgun"), :shotgun}, | ||||
|             {gettext("Pistol"), :pistol} | ||||
|           ], | ||||
|           class: "mx-2 my-1 min-w-md input input-primary", | ||||
|           value: @type | ||||
|         ) %> | ||||
|       </.form> | ||||
|  | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:search} | ||||
|         phx-change="search" | ||||
|         phx-submit="search" | ||||
|         class="grow self-stretch flex flex-col items-stretch" | ||||
|         class="grow flex items-center" | ||||
|       > | ||||
|         <%= text_input(f, :search_term, | ||||
|           class: "input input-primary", | ||||
|           class: "grow input input-primary", | ||||
|           value: @search, | ||||
|           role: "search", | ||||
|           phx_debounce: 300, | ||||
| @@ -55,6 +79,7 @@ | ||||
|         ammo_types={@ammo_types} | ||||
|         current_user={@current_user} | ||||
|         show_used={@show_used} | ||||
|         type={@type} | ||||
|       > | ||||
|         <:actions :let={ammo_type}> | ||||
|           <div class="px-4 py-2 space-x-4 flex justify-center items-center"> | ||||
|   | ||||
| @@ -7,28 +7,6 @@ defmodule CanneryWeb.AmmoTypeLive.Show do | ||||
|   alias Cannery.{ActivityLog, Ammo, Ammo.AmmoType, Containers} | ||||
|   alias CanneryWeb.Endpoint | ||||
|  | ||||
|   @fields_list [ | ||||
|     %{label: gettext("Bullet type:"), key: :bullet_type, type: :string}, | ||||
|     %{label: gettext("Bullet core:"), key: :bullet_core, type: :string}, | ||||
|     %{label: gettext("Cartridge:"), key: :cartridge, type: :string}, | ||||
|     %{label: gettext("Caliber:"), key: :caliber, type: :string}, | ||||
|     %{label: gettext("Case material:"), key: :case_material, type: :string}, | ||||
|     %{label: gettext("Jacket type:"), key: :jacket_type, type: :string}, | ||||
|     %{label: gettext("Muzzle velocity:"), key: :muzzle_velocity, type: :string}, | ||||
|     %{label: gettext("Powder type:"), key: :powder_type, type: :string}, | ||||
|     %{label: gettext("Powder grains per charge:"), key: :powder_grains_per_charge, type: :string}, | ||||
|     %{label: gettext("Grains:"), key: :grains, type: :string}, | ||||
|     %{label: gettext("Pressure:"), key: :pressure, type: :string}, | ||||
|     %{label: gettext("Primer type:"), key: :primer_type, type: :string}, | ||||
|     %{label: gettext("Firing type:"), key: :firing_type, type: :string}, | ||||
|     %{label: gettext("Tracer:"), key: :tracer, type: :boolean}, | ||||
|     %{label: gettext("Incendiary:"), key: :incendiary, type: :boolean}, | ||||
|     %{label: gettext("Blank:"), key: :blank, type: :boolean}, | ||||
|     %{label: gettext("Corrosive:"), key: :corrosive, type: :boolean}, | ||||
|     %{label: gettext("Manufacturer:"), key: :manufacturer, type: :string}, | ||||
|     %{label: gettext("UPC:"), key: :upc, type: :string} | ||||
|   ] | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket), | ||||
|     do: {:ok, socket |> assign(show_used: false, view_table: true)} | ||||
| @@ -65,8 +43,8 @@ defmodule CanneryWeb.AmmoTypeLive.Show do | ||||
|            socket, | ||||
|          %AmmoType{name: ammo_type_name} = ammo_type | ||||
|        ) do | ||||
|     fields_to_display = | ||||
|       @fields_list | ||||
|     custom_fields? = | ||||
|       fields_to_display(ammo_type) | ||||
|       |> Enum.any?(fn %{key: field, type: type} -> | ||||
|         default_value = | ||||
|           case type do | ||||
| @@ -125,8 +103,8 @@ defmodule CanneryWeb.AmmoTypeLive.Show do | ||||
|       packs_count: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user), | ||||
|       used_packs_count: used_packs_count, | ||||
|       historical_packs_count: historical_packs_count, | ||||
|       fields_list: @fields_list, | ||||
|       fields_to_display: fields_to_display | ||||
|       fields_to_display: fields_to_display(ammo_type), | ||||
|       custom_fields?: custom_fields? | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @@ -138,6 +116,48 @@ defmodule CanneryWeb.AmmoTypeLive.Show do | ||||
|     socket |> display_ammo_type(ammo_type) | ||||
|   end | ||||
|  | ||||
|   defp fields_to_display(%AmmoType{type: type}) do | ||||
|     [ | ||||
|       %{label: gettext("Cartridge:"), key: :cartridge, type: :string}, | ||||
|       %{ | ||||
|         label: if(type == :shotgun, do: gettext("Gauge:"), else: gettext("Caliber:")), | ||||
|         key: :caliber, | ||||
|         type: :string | ||||
|       }, | ||||
|       %{label: gettext("Unfired length:"), key: :unfired_length, type: :string}, | ||||
|       %{label: gettext("Brass height:"), key: :brass_height, type: :string}, | ||||
|       %{label: gettext("Chamber size:"), key: :chamber_size, type: :string}, | ||||
|       %{label: gettext("Grains:"), key: :grains, type: :string}, | ||||
|       %{label: gettext("Bullet type:"), key: :bullet_type, type: :string}, | ||||
|       %{label: gettext("Bullet core:"), key: :bullet_core, type: :string}, | ||||
|       %{label: gettext("Jacket type:"), key: :jacket_type, type: :string}, | ||||
|       %{label: gettext("Case material:"), key: :case_material, type: :string}, | ||||
|       %{label: gettext("Wadding:"), key: :wadding, type: :string}, | ||||
|       %{label: gettext("Shot type:"), key: :shot_type, type: :string}, | ||||
|       %{label: gettext("Shot material:"), key: :shot_material, type: :string}, | ||||
|       %{label: gettext("Shot size:"), key: :shot_size, type: :string}, | ||||
|       %{label: gettext("Load grains:"), key: :load_grains, type: :string}, | ||||
|       %{label: gettext("Shot charge weight:"), key: :shot_charge_weight, type: :string}, | ||||
|       %{label: gettext("Powder type:"), key: :powder_type, type: :string}, | ||||
|       %{ | ||||
|         label: gettext("Powder grains per charge:"), | ||||
|         key: :powder_grains_per_charge, | ||||
|         type: :string | ||||
|       }, | ||||
|       %{label: gettext("Pressure:"), key: :pressure, type: :string}, | ||||
|       %{label: gettext("Dram equivalent:"), key: :dram_equivalent, type: :string}, | ||||
|       %{label: gettext("Muzzle velocity:"), key: :muzzle_velocity, type: :string}, | ||||
|       %{label: gettext("Primer type:"), key: :primer_type, type: :string}, | ||||
|       %{label: gettext("Firing type:"), key: :firing_type, type: :string}, | ||||
|       %{label: gettext("Tracer:"), key: :tracer, type: :boolean}, | ||||
|       %{label: gettext("Incendiary:"), key: :incendiary, type: :boolean}, | ||||
|       %{label: gettext("Blank:"), key: :blank, type: :boolean}, | ||||
|       %{label: gettext("Corrosive:"), key: :corrosive, type: :boolean}, | ||||
|       %{label: gettext("Manufacturer:"), key: :manufacturer, type: :string}, | ||||
|       %{label: gettext("UPC:"), key: :upc, type: :string} | ||||
|     ] | ||||
|   end | ||||
|  | ||||
|   @spec display_currency(float()) :: String.t() | ||||
|   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) | ||||
| end | ||||
|   | ||||
| @@ -42,9 +42,26 @@ | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <%= if @fields_to_display do %> | ||||
|   <%= if @ammo_type.type || @custom_fields? do %> | ||||
|     <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center"> | ||||
|       <%= for %{label: label, key: key, type: type} <- @fields_list do %> | ||||
|       <h3 class="title text-lg"> | ||||
|         <%= gettext("Type") %> | ||||
|       </h3> | ||||
|  | ||||
|       <span class="text-primary-600"> | ||||
|         <%= case @ammo_type.type do %> | ||||
|           <% :shotgun -> %> | ||||
|             <%= gettext("Shotgun") %> | ||||
|           <% :rifle -> %> | ||||
|             <%= gettext("Rifle") %> | ||||
|           <% :pistol -> %> | ||||
|             <%= gettext("Pistol") %> | ||||
|           <% _ -> %> | ||||
|             <%= gettext("None specified") %> | ||||
|         <% end %> | ||||
|       </span> | ||||
|  | ||||
|       <%= for %{label: label, key: key, type: type} <- @fields_to_display do %> | ||||
|         <%= if @ammo_type |> Map.get(key) do %> | ||||
|           <h3 class="title text-lg"> | ||||
|             <%= label %> | ||||
|   | ||||
| @@ -17,17 +17,17 @@ | ||||
|       <%= dgettext("actions", "New Container") %> | ||||
|     </.link> | ||||
|  | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl"> | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl"> | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:search} | ||||
|         phx-change="search" | ||||
|         phx-submit="search" | ||||
|         class="grow self-stretch flex flex-col items-stretch" | ||||
|         class="grow flex items-center" | ||||
|       > | ||||
|         <%= text_input(f, :search_term, | ||||
|           class: "input input-primary", | ||||
|           class: "grow input input-primary", | ||||
|           value: @search, | ||||
|           role: "search", | ||||
|           phx_debounce: 300, | ||||
| @@ -41,80 +41,22 @@ | ||||
|         </span> | ||||
|       </.toggle_button> | ||||
|     </div> | ||||
|   <% end %> | ||||
|  | ||||
|   <%= if @containers |> Enum.empty?() do %> | ||||
|     <h2 class="title text-xl text-primary-600"> | ||||
|       <%= gettext("No containers") %> | ||||
|       <%= display_emoji("😔") %> | ||||
|     </h2> | ||||
|   <% else %> | ||||
|     <%= if @view_table do %> | ||||
|       <.live_component | ||||
|         module={CanneryWeb.Components.ContainerTableComponent} | ||||
|         id="containers_index_table" | ||||
|         action={@live_action} | ||||
|         containers={@containers} | ||||
|         current_user={@current_user} | ||||
|       > | ||||
|         <:tag_actions :let={container}> | ||||
|           <div class="mx-4 my-2"> | ||||
|             <.link | ||||
|               patch={Routes.container_index_path(Endpoint, :edit_tags, container)} | ||||
|               class="text-primary-600 link" | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Tag %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-tags"></i> | ||||
|             </.link> | ||||
|           </div> | ||||
|         </:tag_actions> | ||||
|         <:actions :let={container}> | ||||
|           <.link | ||||
|             patch={Routes.container_index_path(Endpoint, :edit, container)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|           </.link> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.container_index_path(Endpoint, :clone, container)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|           </.link> | ||||
|  | ||||
|           <.link | ||||
|             href="#" | ||||
|             class="text-primary-600 link" | ||||
|             phx-click="delete" | ||||
|             phx-value-id={container.id} | ||||
|             data-confirm={ | ||||
|               dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name) | ||||
|             } | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Delete %{container_name}", container_name: container.name) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           </.link> | ||||
|         </:actions> | ||||
|       </.live_component> | ||||
|     <%= if @containers |> Enum.empty?() do %> | ||||
|       <h2 class="title text-xl text-primary-600"> | ||||
|         <%= gettext("No containers") %> | ||||
|         <%= display_emoji("😔") %> | ||||
|       </h2> | ||||
|     <% else %> | ||||
|       <div class="w-full flex flex-row flex-wrap justify-center items-stretch"> | ||||
|         <.container_card | ||||
|           :for={container <- @containers} | ||||
|           container={container} | ||||
|       <%= if @view_table do %> | ||||
|         <.live_component | ||||
|           module={CanneryWeb.Components.ContainerTableComponent} | ||||
|           id="containers_index_table" | ||||
|           action={@live_action} | ||||
|           containers={@containers} | ||||
|           current_user={@current_user} | ||||
|         > | ||||
|           <:tag_actions> | ||||
|           <:tag_actions :let={container}> | ||||
|             <div class="mx-4 my-2"> | ||||
|               <.link | ||||
|                 patch={Routes.container_index_path(Endpoint, :edit_tags, container)} | ||||
| @@ -127,42 +69,104 @@ | ||||
|               </.link> | ||||
|             </div> | ||||
|           </:tag_actions> | ||||
|           <.link | ||||
|             patch={Routes.container_index_path(Endpoint, :edit, container)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|           </.link> | ||||
|           <:actions :let={container}> | ||||
|             <.link | ||||
|               patch={Routes.container_index_path(Endpoint, :edit, container)} | ||||
|               class="text-primary-600 link" | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|             </.link> | ||||
|  | ||||
|           <.link | ||||
|             patch={Routes.container_index_path(Endpoint, :clone, container)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||
|             } | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|           </.link> | ||||
|             <.link | ||||
|               patch={Routes.container_index_path(Endpoint, :clone, container)} | ||||
|               class="text-primary-600 link" | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|             </.link> | ||||
|  | ||||
|           <.link | ||||
|             href="#" | ||||
|             class="text-primary-600 link" | ||||
|             phx-click="delete" | ||||
|             phx-value-id={container.id} | ||||
|             data-confirm={ | ||||
|               dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name) | ||||
|             } | ||||
|             aria-label={ | ||||
|               dgettext("actions", "Delete %{container_name}", container_name: container.name) | ||||
|             } | ||||
|             <.link | ||||
|               href="#" | ||||
|               class="text-primary-600 link" | ||||
|               phx-click="delete" | ||||
|               phx-value-id={container.id} | ||||
|               data-confirm={ | ||||
|                 dgettext("prompts", "Are you sure you want to delete %{name}?", | ||||
|                   name: container.name | ||||
|                 ) | ||||
|               } | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Delete %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|             </.link> | ||||
|           </:actions> | ||||
|         </.live_component> | ||||
|       <% else %> | ||||
|         <div class="w-full flex flex-row flex-wrap justify-center items-stretch"> | ||||
|           <.container_card | ||||
|             :for={container <- @containers} | ||||
|             container={container} | ||||
|             current_user={@current_user} | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           </.link> | ||||
|         </.container_card> | ||||
|       </div> | ||||
|             <:tag_actions> | ||||
|               <div class="mx-4 my-2"> | ||||
|                 <.link | ||||
|                   patch={Routes.container_index_path(Endpoint, :edit_tags, container)} | ||||
|                   class="text-primary-600 link" | ||||
|                   aria-label={ | ||||
|                     dgettext("actions", "Tag %{container_name}", container_name: container.name) | ||||
|                   } | ||||
|                 > | ||||
|                   <i class="fa-fw fa-lg fas fa-tags"></i> | ||||
|                 </.link> | ||||
|               </div> | ||||
|             </:tag_actions> | ||||
|             <.link | ||||
|               patch={Routes.container_index_path(Endpoint, :edit, container)} | ||||
|               class="text-primary-600 link" | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|             </.link> | ||||
|  | ||||
|             <.link | ||||
|               patch={Routes.container_index_path(Endpoint, :clone, container)} | ||||
|               class="text-primary-600 link" | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-copy"></i> | ||||
|             </.link> | ||||
|  | ||||
|             <.link | ||||
|               href="#" | ||||
|               class="text-primary-600 link" | ||||
|               phx-click="delete" | ||||
|               phx-value-id={container.id} | ||||
|               data-confirm={ | ||||
|                 dgettext("prompts", "Are you sure you want to delete %{name}?", | ||||
|                   name: container.name | ||||
|                 ) | ||||
|               } | ||||
|               aria-label={ | ||||
|                 dgettext("actions", "Delete %{container_name}", container_name: container.name) | ||||
|               } | ||||
|             > | ||||
|               <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|             </.link> | ||||
|           </.container_card> | ||||
|         </div> | ||||
|       <% end %> | ||||
|     <% end %> | ||||
|   <% end %> | ||||
| </div> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ defmodule CanneryWeb.ContainerLive.Show do | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket), | ||||
|     do: {:ok, socket |> assign(show_used: false, view_table: true)} | ||||
|     do: {:ok, socket |> assign(type: :all, view_table: true)} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(%{"id" => id}, _session, %{assigns: %{current_user: current_user}} = socket) do | ||||
| @@ -82,22 +82,34 @@ defmodule CanneryWeb.ContainerLive.Show do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do | ||||
|     {:noreply, socket |> assign(:show_used, !show_used) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do | ||||
|     {:noreply, socket |> assign(:view_table, !view_table) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :rifle) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :shotgun) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :pistol) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :all) |> render_container()} | ||||
|   end | ||||
|  | ||||
|   @spec render_container(Socket.t(), Container.id(), User.t()) :: Socket.t() | ||||
|   defp render_container( | ||||
|          %{assigns: %{live_action: live_action, show_used: show_used}} = socket, | ||||
|          %{assigns: %{type: type, live_action: live_action}} = socket, | ||||
|          id, | ||||
|          current_user | ||||
|        ) do | ||||
|     %{name: container_name} = container = Containers.get_container!(id, current_user) | ||||
|     ammo_groups = Ammo.list_ammo_groups_for_container(container, current_user, show_used) | ||||
|     ammo_groups = Ammo.list_ammo_groups_for_container(container, type, current_user) | ||||
|     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) | ||||
| @@ -113,6 +125,7 @@ defmodule CanneryWeb.ContainerLive.Show do | ||||
|     |> assign( | ||||
|       container: container, | ||||
|       round_count: Ammo.get_round_count_for_container!(container, current_user), | ||||
|       ammo_groups_count: Ammo.get_ammo_groups_count_for_container!(container, current_user), | ||||
|       ammo_groups: ammo_groups, | ||||
|       original_counts: original_counts, | ||||
|       cprs: cprs, | ||||
|   | ||||
| @@ -18,22 +18,15 @@ | ||||
|     <%= @container.location %> | ||||
|   </span> | ||||
|  | ||||
|   <%= unless @ammo_groups |> Enum.empty?() do %> | ||||
|     <span class="rounded-lg title text-lg"> | ||||
|       <%= gettext("Packs:") %> | ||||
|       <%= @ammo_groups |> Enum.reject(fn %{count: count} -> count in [0, nil] end) |> Enum.count() %> | ||||
|     </span> | ||||
|   <span class="rounded-lg title text-lg"> | ||||
|     <%= gettext("Packs:") %> | ||||
|     <%= @ammo_groups_count %> | ||||
|   </span> | ||||
|  | ||||
|     <span :if={@show_used} class="rounded-lg title text-lg"> | ||||
|       <%= gettext("Total packs:") %> | ||||
|       <%= Enum.count(@ammo_groups) %> | ||||
|     </span> | ||||
|  | ||||
|     <span class="rounded-lg title text-lg"> | ||||
|       <%= gettext("Rounds:") %> | ||||
|       <%= @round_count %> | ||||
|     </span> | ||||
|   <% end %> | ||||
|   <span class="rounded-lg title text-lg"> | ||||
|     <%= gettext("Rounds:") %> | ||||
|     <%= @round_count %> | ||||
|   </span> | ||||
|  | ||||
|   <div class="flex space-x-4 justify-center items-center text-primary-600"> | ||||
|     <.link | ||||
| @@ -93,11 +86,29 @@ | ||||
|   <hr class="mb-4 hr" /> | ||||
|  | ||||
|   <div class="flex justify-center items-center space-x-4"> | ||||
|     <.toggle_button action="toggle_show_used" value={@show_used}> | ||||
|       <span class="title text-lg text-primary-600"> | ||||
|         <%= gettext("Show used") %> | ||||
|       </span> | ||||
|     </.toggle_button> | ||||
|     <.form | ||||
|       :let={f} | ||||
|       for={%{}} | ||||
|       as={:ammo_type} | ||||
|       phx-change="change_type" | ||||
|       phx-submit="change_type" | ||||
|       class="flex items-center" | ||||
|     > | ||||
|       <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> | ||||
|  | ||||
|       <%= select( | ||||
|         f, | ||||
|         :type, | ||||
|         [ | ||||
|           {gettext("All"), :all}, | ||||
|           {gettext("Rifle"), :rifle}, | ||||
|           {gettext("Shotgun"), :shotgun}, | ||||
|           {gettext("Pistol"), :pistol} | ||||
|         ], | ||||
|         class: "mx-2 my-1 min-w-md input input-primary", | ||||
|         value: @type | ||||
|       ) %> | ||||
|     </.form> | ||||
|  | ||||
|     <.toggle_button action="toggle_table" value={@view_table}> | ||||
|       <span class="title text-lg text-primary-600"> | ||||
| @@ -118,7 +129,7 @@ | ||||
|           id="ammo-type-show-table" | ||||
|           ammo_groups={@ammo_groups} | ||||
|           current_user={@current_user} | ||||
|           show_used={@show_used} | ||||
|           show_used={false} | ||||
|         > | ||||
|           <:ammo_type :let={%{name: ammo_type_name} = ammo_type}> | ||||
|             <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link"> | ||||
|   | ||||
| @@ -10,11 +10,11 @@ defmodule CanneryWeb.RangeLive.Index do | ||||
|  | ||||
|   @impl true | ||||
|   def mount(%{"search" => search}, _session, socket) do | ||||
|     {:ok, socket |> assign(search: search) |> display_shot_groups()} | ||||
|     {:ok, socket |> assign(type: :all, search: search) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket |> assign(search: nil) |> display_shot_groups()} | ||||
|     {:ok, socket |> assign(type: :all, search: nil) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
| @@ -102,9 +102,27 @@ defmodule CanneryWeb.RangeLive.Index do | ||||
|     {:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :search, search_term))} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :rifle) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :shotgun) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :pistol) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do | ||||
|     {:noreply, socket |> assign(:type, :all) |> display_shot_groups()} | ||||
|   end | ||||
|  | ||||
|   @spec display_shot_groups(Socket.t()) :: Socket.t() | ||||
|   defp display_shot_groups(%{assigns: %{search: search, current_user: current_user}} = socket) do | ||||
|     shot_groups = ActivityLog.list_shot_groups(search, current_user) | ||||
|   defp display_shot_groups( | ||||
|          %{assigns: %{type: type, search: search, current_user: current_user}} = socket | ||||
|        ) do | ||||
|     shot_groups = ActivityLog.list_shot_groups(search, type, current_user) | ||||
|     ammo_groups = Ammo.list_staged_ammo_groups(current_user) | ||||
|     chart_data = shot_groups |> get_chart_data_for_shot_group() | ||||
|     original_counts = ammo_groups |> Ammo.get_original_counts(current_user) | ||||
|   | ||||
| @@ -74,17 +74,41 @@ | ||||
|       <%= dgettext("errors", "Your browser does not support the canvas element.") %> | ||||
|     </canvas> | ||||
|  | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl"> | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl"> | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:ammo_type} | ||||
|         phx-change="change_type" | ||||
|         phx-submit="change_type" | ||||
|         class="flex items-center" | ||||
|       > | ||||
|         <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> | ||||
|  | ||||
|         <%= select( | ||||
|           f, | ||||
|           :type, | ||||
|           [ | ||||
|             {gettext("All"), :all}, | ||||
|             {gettext("Rifle"), :rifle}, | ||||
|             {gettext("Shotgun"), :shotgun}, | ||||
|             {gettext("Pistol"), :pistol} | ||||
|           ], | ||||
|           class: "mx-2 my-1 min-w-md input input-primary", | ||||
|           value: @type | ||||
|         ) %> | ||||
|       </.form> | ||||
|  | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:search} | ||||
|         phx-change="search" | ||||
|         phx-submit="search" | ||||
|         class="grow self-stretch flex flex-col items-stretch" | ||||
|         class="grow flex items-center" | ||||
|       > | ||||
|         <%= text_input(f, :search_term, | ||||
|           class: "input input-primary", | ||||
|           class: "grow input input-primary", | ||||
|           value: @search, | ||||
|           role: "search", | ||||
|           phx_debounce: 300, | ||||
|   | ||||
| @@ -18,57 +18,57 @@ | ||||
|     <.link patch={Routes.tag_index_path(Endpoint, :new)} class="btn btn-primary"> | ||||
|       <%= dgettext("actions", "New Tag") %> | ||||
|     </.link> | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl"> | ||||
|     <.form | ||||
|       :let={f} | ||||
|       for={%{}} | ||||
|       as={:search} | ||||
|       phx-change="search" | ||||
|       phx-submit="search" | ||||
|       class="grow self-stretch flex flex-col items-stretch" | ||||
|     > | ||||
|       <%= text_input(f, :search_term, | ||||
|         class: "input input-primary", | ||||
|         value: @search, | ||||
|         role: "search", | ||||
|         phx_debounce: 300, | ||||
|         placeholder: gettext("Search tags") | ||||
|       ) %> | ||||
|     </.form> | ||||
|   </div> | ||||
|  | ||||
|   <%= if @tags |> Enum.empty?() do %> | ||||
|     <h2 class="title text-xl text-primary-600"> | ||||
|       <%= gettext("No tags") %> | ||||
|       <%= display_emoji("😔") %> | ||||
|     </h2> | ||||
|   <% else %> | ||||
|     <div class="flex flex-row flex-wrap justify-center items-stretch"> | ||||
|       <.tag_card :for={tag <- @tags} tag={tag}> | ||||
|         <.link | ||||
|           patch={Routes.tag_index_path(Endpoint, :edit, tag)} | ||||
|           class="text-primary-600 link" | ||||
|           aria-label={dgettext("actions", "Edit %{tag_name}", tag_name: tag.name)} | ||||
|         > | ||||
|           <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|         </.link> | ||||
|  | ||||
|         <.link | ||||
|           href="#" | ||||
|           class="text-primary-600 link" | ||||
|           phx-click="delete" | ||||
|           phx-value-id={tag.id} | ||||
|           data-confirm={ | ||||
|             dgettext("prompts", "Are you sure you want to delete %{name}?", name: tag.name) | ||||
|           } | ||||
|           aria-label={dgettext("actions", "Delete %{tag_name}", tag_name: tag.name)} | ||||
|         > | ||||
|           <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|         </.link> | ||||
|       </.tag_card> | ||||
|     <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-2xl"> | ||||
|       <.form | ||||
|         :let={f} | ||||
|         for={%{}} | ||||
|         as={:search} | ||||
|         phx-change="search" | ||||
|         phx-submit="search" | ||||
|         class="grow flex items-center" | ||||
|       > | ||||
|         <%= text_input(f, :search_term, | ||||
|           class: "grow input input-primary", | ||||
|           value: @search, | ||||
|           role: "search", | ||||
|           phx_debounce: 300, | ||||
|           placeholder: gettext("Search tags") | ||||
|         ) %> | ||||
|       </.form> | ||||
|     </div> | ||||
|  | ||||
|     <%= if @tags |> Enum.empty?() do %> | ||||
|       <h2 class="title text-xl text-primary-600"> | ||||
|         <%= gettext("No tags") %> | ||||
|         <%= display_emoji("😔") %> | ||||
|       </h2> | ||||
|     <% else %> | ||||
|       <div class="flex flex-row flex-wrap justify-center items-stretch"> | ||||
|         <.tag_card :for={tag <- @tags} tag={tag}> | ||||
|           <.link | ||||
|             patch={Routes.tag_index_path(Endpoint, :edit, tag)} | ||||
|             class="text-primary-600 link" | ||||
|             aria-label={dgettext("actions", "Edit %{tag_name}", tag_name: tag.name)} | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|           </.link> | ||||
|  | ||||
|           <.link | ||||
|             href="#" | ||||
|             class="text-primary-600 link" | ||||
|             phx-click="delete" | ||||
|             phx-value-id={tag.id} | ||||
|             data-confirm={ | ||||
|               dgettext("prompts", "Are you sure you want to delete %{name}?", name: tag.name) | ||||
|             } | ||||
|             aria-label={dgettext("actions", "Delete %{tag_name}", tag_name: tag.name)} | ||||
|           > | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           </.link> | ||||
|         </.tag_card> | ||||
|       </div> | ||||
|     <% end %> | ||||
|   <% end %> | ||||
| </div> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user