Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
8c95536ffd | |||
d9251c7e4c | |||
fe4e4f4f17 | |||
e5e5449e8b | |||
355752598c | |||
03f8a2e8a7 | |||
071eb1b3c9 | |||
2987e4ff37 | |||
ca81924ebe | |||
40e4f6fe0a | |||
213dcca973 | |||
b32edd581d | |||
2e372ca2ab | |||
fd0bac3bbf | |||
f83fbc5d99 | |||
daab051026 | |||
440dc5061b | |||
c0d2c69144 | |||
7a7359fa66 | |||
9e8fd00d65 | |||
f5f72b53e6 | |||
a54cf8b87d | |||
0b7146ba32 | |||
c0441957b6 | |||
7fa9933a9b | |||
f4c7f22460 | |||
a01d97e360 | |||
a53b352cf7 | |||
ce07cc2569 | |||
3acecb9a93 | |||
ab8561fcf0 | |||
8163b906a2 |
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,3 +1,35 @@
|
||||
# v0.9.0
|
||||
- Add length limits to all string fields
|
||||
- Add selectable ammo types
|
||||
- Improve onboarding experience slightly
|
||||
- Remove show used view from a container since it doesn't really make that much
|
||||
sense
|
||||
|
||||
# v0.8.6
|
||||
- Fix duplicate entries showing up
|
||||
- Show ammo packs under a type in a table by default
|
||||
- Only show historical ammo type information when displaying "Show used" in table
|
||||
- Only show historical ammo pack information when displaying "Show used" in table
|
||||
- Fix some values not being sorted in tables properly
|
||||
- Code quality improvements
|
||||
- Show link to ammo pack in ammo pack table while viewing ammo type
|
||||
|
||||
# v0.8.5
|
||||
- Add link in readme to github mirror
|
||||
- Fix tables unable to sort on empty dates
|
||||
- Only show historical ammo type information when displaying "Show used"
|
||||
- Fix even more accessibility issues
|
||||
|
||||
# v0.8.4
|
||||
- Improve accessibility
|
||||
- Code quality improvements
|
||||
- Fix dead link of example bullet abbreviations
|
||||
- Fix inaccurate error message when updating shot records
|
||||
- Fix tables not sorting dates correctly
|
||||
- Fix dates displaying incorrectly
|
||||
- Fix container table not displaying all fields
|
||||
- Fix textareas resizing when typing in them
|
||||
|
||||
# v0.8.3
|
||||
- Improve some styles
|
||||
- Improve server log
|
||||
|
@ -92,6 +92,15 @@ Cannery is licensed under AGPLv3 or later. A copy of the latest version of the
|
||||
license can be found at
|
||||
[LICENSE.md](https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/LICENSE.md).
|
||||
|
||||
# Links
|
||||
|
||||
- [Gitea](https://gitea.bubbletea.dev/shibao/cannery): Main repo, feature
|
||||
requests and bug reports
|
||||
- [Github](https://github.com/shibaobun/cannery): Source code mirror, please
|
||||
don't open pull requests to this repository
|
||||
- [Weblate](https://weblate.bubbletea.dev/engage/cannery): Contribute to
|
||||
translations!
|
||||
|
||||
---
|
||||
|
||||
[![Build
|
||||
|
@ -27,23 +27,15 @@ import { LiveSocket } from 'phoenix_live_view'
|
||||
import topbar from 'topbar'
|
||||
import MaintainAttrs from './maintain_attrs'
|
||||
import ShotLogChart from './shot_log_chart'
|
||||
import Alpine from 'alpinejs'
|
||||
import Date from './date'
|
||||
import DateTime from './datetime'
|
||||
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
||||
const liveSocket = new LiveSocket('/live', Socket, {
|
||||
dom: {
|
||||
onBeforeElUpdated (from, to) {
|
||||
if (from._x_dataStack) { window.Alpine.clone(from, to) }
|
||||
}
|
||||
},
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: { MaintainAttrs, ShotLogChart }
|
||||
hooks: { Date, DateTime, MaintainAttrs, ShotLogChart }
|
||||
})
|
||||
|
||||
// alpine.js
|
||||
window.Alpine = Alpine
|
||||
Alpine.start()
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' })
|
||||
window.addEventListener('phx:page-loading-start', info => topbar.show())
|
||||
|
11
assets/js/date.js
Normal file
11
assets/js/date.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
displayDate (el) {
|
||||
const date =
|
||||
Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' })
|
||||
.format(new Date(el.dateTime))
|
||||
|
||||
el.innerText = date
|
||||
},
|
||||
mounted () { this.displayDate(this.el) },
|
||||
updated () { this.displayDate(this.el) }
|
||||
}
|
11
assets/js/datetime.js
Normal file
11
assets/js/datetime.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
displayDateTime (el) {
|
||||
const date =
|
||||
Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'long' })
|
||||
.format(new Date(el.dateTime))
|
||||
|
||||
el.innerText = date
|
||||
},
|
||||
mounted () { this.displayDateTime(this.el) },
|
||||
updated () { this.displayDateTime(this.el) }
|
||||
}
|
9280
assets/package-lock.json
generated
9280
assets/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "v18.9.1",
|
||||
"npm": "8.10.0"
|
||||
"npm": "8.19.1"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "NODE_ENV=production webpack --mode production",
|
||||
@ -13,37 +13,37 @@
|
||||
"test": "standard"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"alpinejs": "^3.10.2",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"chart.js": "^4.2.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"topbar": "^1.0.1"
|
||||
"topbar": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.10",
|
||||
"@babel/preset-env": "^7.17.10",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-loader": "^8.2.5",
|
||||
"copy-webpack-plugin": "^10.2.4",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.2",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^4.2.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-loader": "^6.2.1",
|
||||
"postcss-preset-env": "^7.5.0",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"npm-check-updates": "^16.7.12",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.0.1",
|
||||
"sass": "^1.59.3",
|
||||
"sass-loader": "^13.2.1",
|
||||
"standard": "^17.0.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"terser-webpack-plugin": "^5.3.1",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.0"
|
||||
"tailwindcss": "^3.2.7",
|
||||
"terser-webpack-plugin": "^5.3.7",
|
||||
"webpack": "^5.76.2",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -385,8 +385,18 @@ defmodule Cannery.Accounts do
|
||||
"""
|
||||
@spec allow_registration?() :: boolean()
|
||||
def allow_registration? do
|
||||
Application.get_env(:cannery, Cannery.Accounts)[:registration] == "public" or
|
||||
list_users_by_role(:admin) |> Enum.empty?()
|
||||
registration_mode() == :public or list_users_by_role(:admin) |> Enum.empty?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an atom representing the current configured registration mode
|
||||
"""
|
||||
@spec registration_mode() :: :public | :invite_only
|
||||
def registration_mode do
|
||||
case Application.get_env(:cannery, Cannery.Accounts)[:registration] do
|
||||
"public" -> :public
|
||||
_other -> :invite_only
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -48,8 +48,9 @@ defmodule Cannery.Accounts.Invite do
|
||||
%__MODULE__{}
|
||||
|> change(token: token, created_by_id: user_id)
|
||||
|> cast(attrs, [:name, :uses_left, :disabled_at])
|
||||
|> validate_required([:name, :token, :created_by_id])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_number(:uses_left, greater_than_or_equal_to: 0)
|
||||
|> validate_required([:name, :token, :created_by_id])
|
||||
end
|
||||
|
||||
@doc false
|
||||
@ -57,7 +58,8 @@ defmodule Cannery.Accounts.Invite do
|
||||
def update_changeset(invite, attrs) do
|
||||
invite
|
||||
|> cast(attrs, [:name, :uses_left, :disabled_at])
|
||||
|> validate_required([:name])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_number(:uses_left, greater_than_or_equal_to: 0)
|
||||
|> validate_required([:name])
|
||||
end
|
||||
end
|
||||
|
@ -100,13 +100,23 @@ defmodule Cannery.Accounts.Invites do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_use_count(Invite.t(), User.t()) :: non_neg_integer()
|
||||
def get_use_count(%Invite{id: invite_id}, %User{role: :admin}) do
|
||||
Repo.one(
|
||||
@spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | nil
|
||||
def get_use_count(%Invite{id: invite_id} = invite, user) do
|
||||
[invite] |> get_use_counts(user) |> Map.get(invite_id)
|
||||
end
|
||||
|
||||
@spec get_use_counts([Invite.t()], User.t()) ::
|
||||
%{optional(Invite.id()) => non_neg_integer()}
|
||||
def get_use_counts(invites, %User{role: :admin}) do
|
||||
invite_ids = invites |> Enum.map(fn %{id: invite_id} -> invite_id end)
|
||||
|
||||
Repo.all(
|
||||
from u in User,
|
||||
where: u.invite_id == ^invite_id,
|
||||
select: count(u.id)
|
||||
where: u.invite_id in ^invite_ids,
|
||||
group_by: u.invite_id,
|
||||
select: {u.invite_id, count(u.id)}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
@spec decrement_invite_changeset(Invite.t()) :: Invite.changeset()
|
||||
|
@ -79,6 +79,7 @@ defmodule Cannery.Accounts.User do
|
||||
%User{}
|
||||
|> cast(attrs, [:email, :password, :locale])
|
||||
|> put_change(:invite_id, if(invite, do: invite.id))
|
||||
|> validate_length(:locale, max: 255)
|
||||
|> validate_email()
|
||||
|> validate_password(opts)
|
||||
end
|
||||
@ -209,6 +210,7 @@ defmodule Cannery.Accounts.User do
|
||||
def locale_changeset(user_or_changeset, locale) do
|
||||
user_or_changeset
|
||||
|> cast(%{"locale" => locale}, [:locale])
|
||||
|> validate_length(:locale, max: 255)
|
||||
|> validate_required(:locale)
|
||||
end
|
||||
end
|
||||
|
@ -4,37 +4,55 @@ defmodule Cannery.ActivityLog do
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
|
||||
alias Ecto.Multi
|
||||
alias Cannery.Ammo.{AmmoGroup, AmmoType}
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo}
|
||||
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 assoc(sg, :ammo_group),
|
||||
left_join: at in assoc(ag, :ammo_type),
|
||||
where: sg.user_id == ^user_id,
|
||||
where:
|
||||
query
|
||||
|> where(
|
||||
[sg: sg, ag: ag, at: at],
|
||||
fragment(
|
||||
"? @@ websearch_to_tsquery('english', ?)",
|
||||
sg.search,
|
||||
@ -49,15 +67,40 @@ defmodule Cannery.ActivityLog do
|
||||
"? @@ websearch_to_tsquery('english', ?)",
|
||||
at.search,
|
||||
^trimmed_search
|
||||
),
|
||||
order_by: {
|
||||
)
|
||||
)
|
||||
|> 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},
|
||||
%User{id: user_id}
|
||||
) do
|
||||
Repo.all(
|
||||
from sg in ShotGroup,
|
||||
where: sg.ammo_group_id == ^ammo_group_id,
|
||||
where: sg.user_id == ^user_id
|
||||
)
|
||||
end
|
||||
|
||||
@ -107,9 +150,15 @@ defmodule Cannery.ActivityLog do
|
||||
)
|
||||
|> Multi.run(
|
||||
:ammo_group,
|
||||
fn repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
|
||||
{:ok,
|
||||
repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)}
|
||||
fn _repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
|
||||
ammo_group =
|
||||
Repo.one(
|
||||
from ag in AmmoGroup,
|
||||
where: ag.id == ^ammo_group_id,
|
||||
where: ag.user_id == ^user_id
|
||||
)
|
||||
|
||||
{:ok, ammo_group}
|
||||
end
|
||||
)
|
||||
|> Multi.update(
|
||||
@ -220,4 +269,112 @@ defmodule Cannery.ActivityLog do
|
||||
{:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of shot rounds for an ammo group
|
||||
"""
|
||||
@spec get_used_count(AmmoGroup.t(), User.t()) :: non_neg_integer()
|
||||
def get_used_count(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
|
||||
[ammo_group]
|
||||
|> get_used_counts(user)
|
||||
|> Map.get(ammo_group_id, 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of shot rounds for multiple ammo groups
|
||||
"""
|
||||
@spec get_used_counts([AmmoGroup.t()], User.t()) ::
|
||||
%{optional(AmmoGroup.id()) => non_neg_integer()}
|
||||
def get_used_counts(ammo_groups, %User{id: user_id}) do
|
||||
ammo_group_ids =
|
||||
ammo_groups
|
||||
|> Enum.map(fn %{id: ammo_group_id} -> ammo_group_id end)
|
||||
|
||||
Repo.all(
|
||||
from sg in ShotGroup,
|
||||
where: sg.ammo_group_id in ^ammo_group_ids,
|
||||
where: sg.user_id == ^user_id,
|
||||
group_by: sg.ammo_group_id,
|
||||
select: {sg.ammo_group_id, sum(sg.count)}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last entered shot group date for an ammo group
|
||||
"""
|
||||
@spec get_last_used_date(AmmoGroup.t(), User.t()) :: Date.t() | nil
|
||||
def get_last_used_date(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
|
||||
[ammo_group]
|
||||
|> get_last_used_dates(user)
|
||||
|> Map.get(ammo_group_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last entered shot group date for an ammo group
|
||||
"""
|
||||
@spec get_last_used_dates([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => Date.t()}
|
||||
def get_last_used_dates(ammo_groups, %User{id: user_id}) do
|
||||
ammo_group_ids =
|
||||
ammo_groups
|
||||
|> Enum.map(fn %AmmoGroup{id: ammo_group_id, user_id: ^user_id} -> ammo_group_id end)
|
||||
|
||||
Repo.all(
|
||||
from sg in ShotGroup,
|
||||
where: sg.ammo_group_id in ^ammo_group_ids,
|
||||
where: sg.user_id == ^user_id,
|
||||
group_by: sg.ammo_group_id,
|
||||
select: {sg.ammo_group_id, max(sg.date)}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the total number of rounds shot for an ammo type
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Ammo type does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_used_count_for_ammo_type(123, %User{id: 123})
|
||||
35
|
||||
|
||||
iex> get_used_count_for_ammo_type(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
|
||||
def get_used_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
|
||||
[ammo_type]
|
||||
|> get_used_count_for_ammo_types(user)
|
||||
|> Map.get(ammo_type_id, 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the total number of rounds shot for multiple ammo types
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_used_count_for_ammo_types(123, %User{id: 123})
|
||||
35
|
||||
|
||||
"""
|
||||
@spec get_used_count_for_ammo_types([AmmoType.t()], User.t()) ::
|
||||
%{optional(AmmoType.id()) => non_neg_integer()}
|
||||
def get_used_count_for_ammo_types(ammo_types, %User{id: user_id}) do
|
||||
ammo_type_ids =
|
||||
ammo_types
|
||||
|> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
|
||||
|
||||
Repo.all(
|
||||
from ag in AmmoGroup,
|
||||
left_join: sg in ShotGroup,
|
||||
on: ag.id == sg.ammo_group_id,
|
||||
where: ag.ammo_type_id in ^ammo_type_ids,
|
||||
where: not (sg.count |> is_nil()),
|
||||
group_by: ag.ammo_type_id,
|
||||
select: {ag.ammo_type_id, sum(sg.count)}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
use Ecto.Schema
|
||||
import CanneryWeb.Gettext
|
||||
import Ecto.Changeset
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
|
||||
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup}
|
||||
alias Ecto.{Changeset, UUID}
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
@ -24,25 +24,23 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
field :date, :date
|
||||
field :notes, :string
|
||||
|
||||
belongs_to :user, User
|
||||
belongs_to :ammo_group, AmmoGroup
|
||||
field :user_id, :binary_id
|
||||
field :ammo_group_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %ShotGroup{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
count: integer,
|
||||
notes: String.t() | nil,
|
||||
date: Date.t() | nil,
|
||||
ammo_group: AmmoGroup.t() | nil,
|
||||
ammo_group_id: AmmoGroup.id(),
|
||||
user: User.t() | nil,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_shot_group :: %ShotGroup{}
|
||||
@type new_shot_group :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_shot_group())
|
||||
|
||||
@ -58,44 +56,52 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
%User{id: user_id},
|
||||
%AmmoGroup{id: ammo_group_id, user_id: user_id} = ammo_group,
|
||||
attrs
|
||||
)
|
||||
when not (user_id |> is_nil()) and not (ammo_group_id |> is_nil()) do
|
||||
) do
|
||||
shot_group
|
||||
|> change(user_id: user_id)
|
||||
|> change(ammo_group_id: ammo_group_id)
|
||||
|> cast(attrs, [:count, :notes, :date])
|
||||
|> validate_number(:count, greater_than: 0)
|
||||
|> validate_length(:notes, max: 255)
|
||||
|> validate_create_shot_group_count(ammo_group)
|
||||
|> validate_required([:count, :date, :ammo_group_id, :user_id])
|
||||
|> validate_required([:date, :ammo_group_id, :user_id])
|
||||
end
|
||||
|
||||
def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do
|
||||
shot_group
|
||||
|> cast(attrs, [:count, :notes, :date])
|
||||
|> validate_number(:count, greater_than: 0)
|
||||
|> validate_required([:count, :ammo_group_id, :user_id])
|
||||
|> validate_length(:notes, max: 255)
|
||||
|> validate_required([:ammo_group_id, :user_id])
|
||||
|> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo pack"))
|
||||
end
|
||||
|
||||
defp validate_create_shot_group_count(changeset, %AmmoGroup{count: ammo_group_count}) do
|
||||
if changeset |> Changeset.get_field(:count) > ammo_group_count do
|
||||
error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count)
|
||||
changeset |> Changeset.add_error(:count, error)
|
||||
else
|
||||
case changeset |> Changeset.get_field(:count) do
|
||||
nil ->
|
||||
changeset |> Changeset.add_error(:ammo_left, dgettext("errors", "can't be blank"))
|
||||
|
||||
count when count > ammo_group_count ->
|
||||
changeset
|
||||
|> Changeset.add_error(:ammo_left, dgettext("errors", "Ammo left must be at least 0"))
|
||||
|
||||
count when count <= 0 ->
|
||||
error =
|
||||
dgettext("errors", "Ammo left can be at most %{count} rounds",
|
||||
count: ammo_group_count - 1
|
||||
)
|
||||
|
||||
changeset |> Changeset.add_error(:ammo_left, error)
|
||||
|
||||
_valid_count ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec update_changeset(t() | new_shot_group(), User.t(), attrs :: map()) :: changeset()
|
||||
def update_changeset(
|
||||
%ShotGroup{user_id: user_id} = shot_group,
|
||||
%User{id: user_id} = user,
|
||||
attrs
|
||||
)
|
||||
when not (user_id |> is_nil()) do
|
||||
def update_changeset(%__MODULE__{} = shot_group, user, attrs) do
|
||||
shot_group
|
||||
|> cast(attrs, [:count, :notes, :date])
|
||||
|> validate_length(:notes, max: 255)
|
||||
|> validate_number(:count, greater_than: 0)
|
||||
|> validate_required([:count, :date])
|
||||
|> validate_update_shot_group_count(shot_group, user)
|
||||
@ -103,25 +109,20 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
|
||||
defp validate_update_shot_group_count(
|
||||
changeset,
|
||||
%ShotGroup{count: count} = shot_group,
|
||||
%User{id: user_id}
|
||||
)
|
||||
when not (user_id |> is_nil()) do
|
||||
%{ammo_group: %AmmoGroup{count: ammo_group_count, user_id: ^user_id}} =
|
||||
shot_group |> Repo.preload(:ammo_group)
|
||||
%__MODULE__{ammo_group_id: ammo_group_id, count: count},
|
||||
user
|
||||
) do
|
||||
%{count: ammo_group_count} = Ammo.get_ammo_group!(ammo_group_id, user)
|
||||
|
||||
new_shot_group_count = changeset |> Changeset.get_field(:count)
|
||||
shot_diff_to_add = new_shot_group_count - count
|
||||
|
||||
cond do
|
||||
shot_diff_to_add > ammo_group_count ->
|
||||
error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count)
|
||||
if shot_diff_to_add > ammo_group_count do
|
||||
error =
|
||||
dgettext("errors", "Count can be at most %{count} shots", count: ammo_group_count + count)
|
||||
|
||||
changeset |> Changeset.add_error(:count, error)
|
||||
|
||||
new_shot_group_count <= 0 ->
|
||||
changeset |> Changeset.add_error(:count, dgettext("errors", "Count must be at least 1"))
|
||||
|
||||
true ->
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,8 @@ defmodule Cannery.Ammo.AmmoGroup do
|
||||
use Ecto.Schema
|
||||
import CanneryWeb.Gettext
|
||||
import Ecto.Changeset
|
||||
alias Cannery.Ammo.{AmmoGroup, AmmoType}
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Containers, Containers.Container}
|
||||
alias Cannery.Ammo.AmmoType
|
||||
alias Cannery.{Accounts.User, Containers, Containers.Container}
|
||||
alias Ecto.{Changeset, UUID}
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
@ -33,15 +33,13 @@ defmodule Cannery.Ammo.AmmoGroup do
|
||||
field :purchased_on, :date
|
||||
|
||||
belongs_to :ammo_type, AmmoType
|
||||
belongs_to :container, Container
|
||||
belongs_to :user, User
|
||||
|
||||
has_many :shot_groups, ShotGroup
|
||||
field :container_id, :binary_id
|
||||
field :user_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %AmmoGroup{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
count: integer,
|
||||
notes: String.t() | nil,
|
||||
@ -50,14 +48,12 @@ defmodule Cannery.Ammo.AmmoGroup do
|
||||
purchased_on: Date.t(),
|
||||
ammo_type: AmmoType.t() | nil,
|
||||
ammo_type_id: AmmoType.id(),
|
||||
container: Container.t() | nil,
|
||||
container_id: Container.id(),
|
||||
user: User.t() | nil,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_ammo_group :: %AmmoGroup{}
|
||||
@type new_ammo_group :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_ammo_group())
|
||||
|
||||
@ -76,8 +72,7 @@ defmodule Cannery.Ammo.AmmoGroup do
|
||||
%User{id: user_id},
|
||||
attrs
|
||||
)
|
||||
when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) and
|
||||
not (user_id |> is_nil()) do
|
||||
when is_binary(ammo_type_id) and is_binary(container_id) and is_binary(user_id) do
|
||||
ammo_group
|
||||
|> change(ammo_type_id: ammo_type_id)
|
||||
|> change(user_id: user_id)
|
||||
|
@ -8,7 +8,7 @@ defmodule Cannery.Ammo.AmmoType do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Cannery.Accounts.User
|
||||
alias Cannery.Ammo.{AmmoGroup, AmmoType}
|
||||
alias Cannery.Ammo.AmmoGroup
|
||||
alias Ecto.{Changeset, UUID}
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
@ -42,39 +42,57 @@ defmodule Cannery.Ammo.AmmoType do
|
||||
field :name, :string
|
||||
field :desc, :string
|
||||
|
||||
# https://en.wikipedia.org/wiki/Bullet#Abbreviations
|
||||
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
|
||||
|
||||
belongs_to :user, User
|
||||
# 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()
|
||||
end
|
||||
|
||||
@type t :: %AmmoType{
|
||||
@type t :: %__MODULE__{
|
||||
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(),
|
||||
@ -95,20 +123,21 @@ defmodule Cannery.Ammo.AmmoType do
|
||||
manufacturer: String.t() | nil,
|
||||
upc: String.t() | nil,
|
||||
user_id: User.id(),
|
||||
user: User.t() | nil,
|
||||
ammo_groups: [AmmoGroup.t()] | nil,
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_ammo_type :: %AmmoType{}
|
||||
@type new_ammo_type :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_ammo_type())
|
||||
@type type :: :rifle | :shotgun | :pistol | nil
|
||||
|
||||
@spec changeset_fields() :: [atom()]
|
||||
defp changeset_fields,
|
||||
do: [
|
||||
:name,
|
||||
:desc,
|
||||
:type,
|
||||
:bullet_type,
|
||||
:bullet_core,
|
||||
:cartridge,
|
||||
@ -122,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,
|
||||
@ -130,20 +169,55 @@ defmodule Cannery.Ammo.AmmoType do
|
||||
:upc
|
||||
]
|
||||
|
||||
@spec string_fields() :: [atom()]
|
||||
defp string_fields,
|
||||
do: [
|
||||
:name,
|
||||
:bullet_type,
|
||||
:bullet_core,
|
||||
:cartridge,
|
||||
:caliber,
|
||||
:case_material,
|
||||
:jacket_type,
|
||||
:powder_type,
|
||||
: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
|
||||
]
|
||||
|
||||
@doc false
|
||||
@spec create_changeset(new_ammo_type(), User.t(), attrs :: map()) :: changeset()
|
||||
def create_changeset(ammo_type, %User{id: user_id}, attrs) do
|
||||
changeset =
|
||||
ammo_type
|
||||
|> change(user_id: user_id)
|
||||
|> cast(attrs, changeset_fields())
|
||||
|
||||
string_fields()
|
||||
|> Enum.reduce(changeset, fn field, acc -> acc |> validate_length(field, max: 255) end)
|
||||
|> validate_required([:name, :user_id])
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec update_changeset(t() | new_ammo_type(), attrs :: map()) :: changeset()
|
||||
def update_changeset(ammo_type, attrs) do
|
||||
changeset =
|
||||
ammo_type
|
||||
|> cast(attrs, changeset_fields())
|
||||
|
||||
string_fields()
|
||||
|> Enum.reduce(changeset, fn field, acc -> acc |> validate_length(field, max: 255) end)
|
||||
|> validate_required(:name)
|
||||
end
|
||||
end
|
||||
|
12
lib/cannery/comparable_date.ex
Normal file
12
lib/cannery/comparable_date.ex
Normal file
@ -0,0 +1,12 @@
|
||||
defmodule Cannery.ComparableDate do
|
||||
@moduledoc """
|
||||
A custom `Date` module that provides a `compare/2` function that is comparable
|
||||
with nil values
|
||||
"""
|
||||
|
||||
@spec compare(Date.t() | any(), Date.t() | any()) :: :lt | :gt | :eq
|
||||
def compare(%Date{} = date_1, %Date{} = date_2), do: Date.compare(date_1, date_2)
|
||||
def compare(%Date{}, _date_2), do: :lt
|
||||
def compare(_date_1, %Date{}), do: :gt
|
||||
def compare(_date_1, _date_2), do: :eq
|
||||
end
|
15
lib/cannery/comparable_datetime.ex
Normal file
15
lib/cannery/comparable_datetime.ex
Normal file
@ -0,0 +1,15 @@
|
||||
defmodule Cannery.ComparableDateTime do
|
||||
@moduledoc """
|
||||
A custom `DateTime` module that provides a `compare/2` function that is
|
||||
comparable with nil values
|
||||
"""
|
||||
|
||||
@spec compare(DateTime.t() | any(), DateTime.t() | any()) :: :lt | :gt | :eq
|
||||
def compare(%DateTime{} = datetime_1, %DateTime{} = datetime_2) do
|
||||
DateTime.compare(datetime_1, datetime_2)
|
||||
end
|
||||
|
||||
def compare(%DateTime{}, _datetime_2), do: :lt
|
||||
def compare(_datetime_1, %DateTime{}), do: :gt
|
||||
def compare(_datetime_1, _datetime_2), do: :eq
|
||||
end
|
@ -5,10 +5,12 @@ defmodule Cannery.Containers do
|
||||
|
||||
import CanneryWeb.Gettext
|
||||
import Ecto.Query, warn: false
|
||||
alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo, Tags.Tag}
|
||||
alias Cannery.Containers.{Container, ContainerTag}
|
||||
alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo}
|
||||
alias Cannery.Containers.{Container, ContainerTag, Tag}
|
||||
alias Ecto.Changeset
|
||||
|
||||
@container_preloads [:tags]
|
||||
|
||||
@doc """
|
||||
Returns the list of containers.
|
||||
|
||||
@ -28,11 +30,10 @@ defmodule Cannery.Containers do
|
||||
as: :c,
|
||||
left_join: t in assoc(c, :tags),
|
||||
as: :t,
|
||||
left_join: ag in assoc(c, :ammo_groups),
|
||||
as: :ag,
|
||||
where: c.user_id == ^user_id,
|
||||
order_by: c.name,
|
||||
preload: [tags: t, ammo_groups: ag]
|
||||
distinct: c.id,
|
||||
preload: ^@container_preloads
|
||||
)
|
||||
|> list_containers_search(search)
|
||||
|> Repo.all()
|
||||
@ -91,7 +92,7 @@ defmodule Cannery.Containers do
|
||||
@doc """
|
||||
Gets a single container.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Container does not exist.
|
||||
Raises `KeyError` if the Container does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
@ -99,20 +100,37 @@ defmodule Cannery.Containers do
|
||||
%Container{}
|
||||
|
||||
iex> get_container!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
** (KeyError)
|
||||
|
||||
"""
|
||||
@spec get_container!(Container.id(), User.t()) :: Container.t()
|
||||
def get_container!(id, %User{id: user_id}) do
|
||||
Repo.one!(
|
||||
def get_container!(id, user) do
|
||||
[id]
|
||||
|> get_containers(user)
|
||||
|> Map.fetch!(id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets multiple containers.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_containers([123], %User{id: 123})
|
||||
%{123 => %Container{}}
|
||||
|
||||
"""
|
||||
@spec get_containers([Container.id()], User.t()) :: %{optional(Container.id()) => Container.t()}
|
||||
def get_containers(ids, %User{id: user_id}) do
|
||||
Repo.all(
|
||||
from c in Container,
|
||||
left_join: t in assoc(c, :tags),
|
||||
left_join: ag in assoc(c, :ammo_groups),
|
||||
where: c.user_id == ^user_id,
|
||||
where: c.id == ^id,
|
||||
where: c.id in ^ids,
|
||||
order_by: c.name,
|
||||
preload: [tags: t, ammo_groups: ag]
|
||||
preload: ^@container_preloads,
|
||||
select: {c.id, c}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -130,7 +148,19 @@ defmodule Cannery.Containers do
|
||||
@spec create_container(attrs :: map(), User.t()) ::
|
||||
{:ok, Container.t()} | {:error, Container.changeset()}
|
||||
def create_container(attrs, %User{} = user) do
|
||||
%Container{} |> Container.create_changeset(user, attrs) |> Repo.insert()
|
||||
%Container{}
|
||||
|> Container.create_changeset(user, attrs)
|
||||
|> Repo.insert()
|
||||
|> case do
|
||||
{:ok, container} -> {:ok, container |> preload_container()}
|
||||
{:error, changeset} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
@spec preload_container(Container.t()) :: Container.t()
|
||||
@spec preload_container([Container.t()]) :: [Container.t()]
|
||||
def preload_container(container) do
|
||||
container |> Repo.preload(@container_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -148,7 +178,13 @@ defmodule Cannery.Containers do
|
||||
@spec update_container(Container.t(), User.t(), attrs :: map()) ::
|
||||
{:ok, Container.t()} | {:error, Container.changeset()}
|
||||
def update_container(%Container{user_id: user_id} = container, %User{id: user_id}, attrs) do
|
||||
container |> Container.update_changeset(attrs) |> Repo.update()
|
||||
container
|
||||
|> Container.update_changeset(attrs)
|
||||
|> Repo.update()
|
||||
|> case do
|
||||
{:ok, container} -> {:ok, container |> preload_container()}
|
||||
{:error, changeset} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -173,7 +209,12 @@ defmodule Cannery.Containers do
|
||||
)
|
||||
|> case do
|
||||
0 ->
|
||||
container |> Repo.delete()
|
||||
container
|
||||
|> Repo.delete()
|
||||
|> case do
|
||||
{:ok, container} -> {:ok, container |> preload_container()}
|
||||
{:error, changeset} -> {:error, changeset}
|
||||
end
|
||||
|
||||
_amount ->
|
||||
error = dgettext("errors", "Container must be empty before deleting")
|
||||
@ -214,8 +255,11 @@ defmodule Cannery.Containers do
|
||||
%Container{user_id: user_id} = container,
|
||||
%Tag{user_id: user_id} = tag,
|
||||
%User{id: user_id}
|
||||
),
|
||||
do: %ContainerTag{} |> ContainerTag.create_changeset(tag, container) |> Repo.insert!()
|
||||
) do
|
||||
%ContainerTag{}
|
||||
|> ContainerTag.create_changeset(tag, container)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a tag from a container
|
||||
@ -226,45 +270,175 @@ defmodule Cannery.Containers do
|
||||
%Container{}
|
||||
|
||||
"""
|
||||
@spec remove_tag!(Container.t(), Tag.t(), User.t()) :: non_neg_integer()
|
||||
@spec remove_tag!(Container.t(), Tag.t(), User.t()) :: {non_neg_integer(), [ContainerTag.t()]}
|
||||
def remove_tag!(
|
||||
%Container{id: container_id, user_id: user_id},
|
||||
%Tag{id: tag_id, user_id: user_id},
|
||||
%User{id: user_id}
|
||||
) do
|
||||
{count, _} =
|
||||
{count, results} =
|
||||
Repo.delete_all(
|
||||
from ct in ContainerTag,
|
||||
where: ct.container_id == ^container_id,
|
||||
where: ct.tag_id == ^tag_id
|
||||
where: ct.tag_id == ^tag_id,
|
||||
select: ct
|
||||
)
|
||||
|
||||
if count == 0, do: raise("could not delete container tag"), else: count
|
||||
if count == 0, do: raise("could not delete container tag"), else: {count, results}
|
||||
end
|
||||
|
||||
# Container Tags
|
||||
|
||||
@doc """
|
||||
Returns the list of tags.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_tags(%User{id: 123})
|
||||
[%Tag{}, ...]
|
||||
|
||||
iex> list_tags("cool", %User{id: 123})
|
||||
[%Tag{name: "my cool tag"}, ...]
|
||||
|
||||
"""
|
||||
@spec list_tags(User.t()) :: [Tag.t()]
|
||||
@spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()]
|
||||
def list_tags(search \\ nil, user)
|
||||
|
||||
def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "",
|
||||
do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name)
|
||||
|
||||
def list_tags(search, %{id: user_id}) when search |> is_binary() do
|
||||
trimmed_search = String.trim(search)
|
||||
|
||||
Repo.all(
|
||||
from t in Tag,
|
||||
where: t.user_id == ^user_id,
|
||||
where:
|
||||
fragment(
|
||||
"? @@ websearch_to_tsquery('english', ?)",
|
||||
t.search,
|
||||
^trimmed_search
|
||||
),
|
||||
order_by: {
|
||||
:desc,
|
||||
fragment(
|
||||
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
|
||||
t.search,
|
||||
^trimmed_search
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns number of rounds in container. If data is already preloaded, then
|
||||
there will be no db hit.
|
||||
Gets a single tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_tag(123, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> get_tag(456, %User{id: 123})
|
||||
{:error, :not_found}
|
||||
|
||||
"""
|
||||
@spec get_container_ammo_group_count!(Container.t()) :: non_neg_integer()
|
||||
def get_container_ammo_group_count!(%Container{} = container) do
|
||||
container
|
||||
|> Repo.preload(:ammo_groups)
|
||||
|> Map.fetch!(:ammo_groups)
|
||||
|> Enum.reject(fn %{count: count} -> count == 0 end)
|
||||
|> Enum.count()
|
||||
@spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, :not_found}
|
||||
def get_tag(id, %User{id: user_id}) do
|
||||
Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
|
||||
|> case do
|
||||
nil -> {:error, :not_found}
|
||||
tag -> {:ok, tag}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns number of rounds in container. If data is already preloaded, then
|
||||
there will be no db hit.
|
||||
Gets a single tag.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Tag does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_tag!(123, %User{id: 123})
|
||||
%Tag{}
|
||||
|
||||
iex> get_tag!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_container_rounds!(Container.t()) :: non_neg_integer()
|
||||
def get_container_rounds!(%Container{} = container) do
|
||||
container
|
||||
|> Repo.preload(:ammo_groups)
|
||||
|> Map.fetch!(:ammo_groups)
|
||||
|> Enum.map(fn %{count: count} -> count end)
|
||||
|> Enum.sum()
|
||||
@spec get_tag!(Tag.id(), User.t()) :: Tag.t()
|
||||
def get_tag!(id, %User{id: user_id}) do
|
||||
Repo.one!(
|
||||
from t in Tag,
|
||||
where: t.id == ^id,
|
||||
where: t.user_id == ^user_id
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_tag(%{field: value}, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> create_tag(%{field: bad_value}, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec create_tag(attrs :: map(), User.t()) ::
|
||||
{:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def create_tag(attrs, %User{} = user) do
|
||||
%Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_tag(tag, %{field: new_value}, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> update_tag(tag, %{field: bad_value}, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}) do
|
||||
tag |> Tag.update_changeset(attrs) |> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_tag(tag, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> delete_tag(tag, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}) do
|
||||
tag |> Repo.delete()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_tag!(tag, %User{id: 123})
|
||||
%Tag{}
|
||||
|
||||
"""
|
||||
@spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
|
||||
def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}) do
|
||||
tag |> Repo.delete!()
|
||||
end
|
||||
end
|
||||
|
@ -6,8 +6,7 @@ defmodule Cannery.Containers.Container do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Cannery.Containers.{Container, ContainerTag}
|
||||
alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag}
|
||||
alias Cannery.{Accounts.User, Containers.ContainerTag, Containers.Tag}
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
@ -26,28 +25,25 @@ defmodule Cannery.Containers.Container do
|
||||
field :location, :string
|
||||
field :type, :string
|
||||
|
||||
belongs_to :user, User
|
||||
field :user_id, :binary_id
|
||||
|
||||
has_many :ammo_groups, AmmoGroup
|
||||
many_to_many :tags, Tag, join_through: ContainerTag
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %Container{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
name: String.t(),
|
||||
desc: String.t(),
|
||||
location: String.t(),
|
||||
type: String.t(),
|
||||
user: User.t(),
|
||||
user_id: User.id(),
|
||||
ammo_groups: [AmmoGroup.t()] | nil,
|
||||
tags: [Tag.t()] | nil,
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_container :: %Container{}
|
||||
@type new_container :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_container())
|
||||
|
||||
@ -57,6 +53,8 @@ defmodule Cannery.Containers.Container do
|
||||
container
|
||||
|> change(user_id: user_id)
|
||||
|> cast(attrs, [:name, :desc, :type, :location])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_length(:type, max: 255)
|
||||
|> validate_required([:name, :type, :user_id])
|
||||
end
|
||||
|
||||
@ -65,6 +63,8 @@ defmodule Cannery.Containers.Container do
|
||||
def update_changeset(container, attrs) do
|
||||
container
|
||||
|> cast(attrs, [:name, :desc, :type, :location])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_length(:type, max: 255)
|
||||
|> validate_required([:name, :type])
|
||||
end
|
||||
end
|
||||
|
@ -1,12 +1,12 @@
|
||||
defmodule Cannery.Containers.ContainerTag do
|
||||
@moduledoc """
|
||||
Thru-table struct for associating Cannery.Containers.Container and
|
||||
Cannery.Tags.Tag.
|
||||
Cannery.Containers.Tag.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Cannery.{Containers.Container, Containers.ContainerTag, Tags.Tag}
|
||||
alias Cannery.Containers.{Container, Tag}
|
||||
alias Ecto.{Changeset, UUID}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@ -18,7 +18,7 @@ defmodule Cannery.Containers.ContainerTag do
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %ContainerTag{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
container: Container.t(),
|
||||
container_id: Container.id(),
|
||||
@ -27,7 +27,7 @@ defmodule Cannery.Containers.ContainerTag do
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_container_tag :: %ContainerTag{}
|
||||
@type new_container_tag :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_container_tag())
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
defmodule Cannery.Tags.Tag do
|
||||
defmodule Cannery.Containers.Tag do
|
||||
@moduledoc """
|
||||
Tags are added to containers to help organize, and can include custom-defined
|
||||
text and bg colors.
|
||||
@ -6,8 +6,8 @@ defmodule Cannery.Tags.Tag do
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Cannery.Accounts.User
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Cannery.{Accounts.User, Tags.Tag}
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
@ -23,22 +23,21 @@ defmodule Cannery.Tags.Tag do
|
||||
field :bg_color, :string
|
||||
field :text_color, :string
|
||||
|
||||
belongs_to :user, User
|
||||
field :user_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %Tag{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
name: String.t(),
|
||||
bg_color: String.t(),
|
||||
text_color: String.t(),
|
||||
user: User.t() | nil,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_tag() :: %Tag{}
|
||||
@type new_tag() :: %__MODULE__{}
|
||||
@type id() :: UUID.t()
|
||||
@type changeset() :: Changeset.t(t() | new_tag())
|
||||
|
||||
@ -48,6 +47,9 @@ defmodule Cannery.Tags.Tag do
|
||||
tag
|
||||
|> change(user_id: user_id)
|
||||
|> cast(attrs, [:name, :bg_color, :text_color])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_length(:bg_color, max: 12)
|
||||
|> validate_length(:text_color, max: 12)
|
||||
|> validate_required([:name, :bg_color, :text_color, :user_id])
|
||||
end
|
||||
|
||||
@ -56,6 +58,9 @@ defmodule Cannery.Tags.Tag do
|
||||
def update_changeset(tag, attrs) do
|
||||
tag
|
||||
|> cast(attrs, [:name, :bg_color, :text_color])
|
||||
|> validate_length(:name, max: 255)
|
||||
|> validate_length(:bg_color, max: 12)
|
||||
|> validate_length(:text_color, max: 12)
|
||||
|> validate_required([:name, :bg_color, :text_color])
|
||||
end
|
||||
end
|
@ -1,149 +0,0 @@
|
||||
defmodule Cannery.Tags do
|
||||
@moduledoc """
|
||||
The Tags context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
import CanneryWeb.Gettext
|
||||
alias Cannery.{Accounts.User, Repo, Tags.Tag}
|
||||
|
||||
@doc """
|
||||
Returns the list of tags.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_tags(%User{id: 123})
|
||||
[%Tag{}, ...]
|
||||
|
||||
iex> list_tags("cool", %User{id: 123})
|
||||
[%Tag{name: "my cool tag"}, ...]
|
||||
|
||||
"""
|
||||
@spec list_tags(User.t()) :: [Tag.t()]
|
||||
@spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()]
|
||||
def list_tags(search \\ nil, user)
|
||||
|
||||
def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "",
|
||||
do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name)
|
||||
|
||||
def list_tags(search, %{id: user_id}) when search |> is_binary() do
|
||||
trimmed_search = String.trim(search)
|
||||
|
||||
Repo.all(
|
||||
from t in Tag,
|
||||
where: t.user_id == ^user_id,
|
||||
where:
|
||||
fragment(
|
||||
"search @@ websearch_to_tsquery('english', ?)",
|
||||
^trimmed_search
|
||||
),
|
||||
order_by: {
|
||||
:desc,
|
||||
fragment(
|
||||
"ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)",
|
||||
^trimmed_search
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_tag(123, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> get_tag(456, %User{id: 123})
|
||||
{:error, "tag not found"}
|
||||
|
||||
"""
|
||||
@spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, String.t()}
|
||||
def get_tag(id, %User{id: user_id}) do
|
||||
Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
|
||||
|> case do
|
||||
nil -> {:error, dgettext("errors", "Tag not found")}
|
||||
tag -> {:ok, tag}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single tag.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Tag does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_tag!(123, %User{id: 123})
|
||||
%Tag{}
|
||||
|
||||
iex> get_tag!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_tag!(Tag.id(), User.t()) :: Tag.t()
|
||||
def get_tag!(id, %User{id: user_id}),
|
||||
do: Repo.one!(from t in Tag, where: t.id == ^id and t.user_id == ^user_id)
|
||||
|
||||
@doc """
|
||||
Creates a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_tag(%{field: value}, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> create_tag(%{field: bad_value}, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec create_tag(attrs :: map(), User.t()) ::
|
||||
{:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def create_tag(attrs, %User{} = user),
|
||||
do: %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert()
|
||||
|
||||
@doc """
|
||||
Updates a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_tag(tag, %{field: new_value}, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> update_tag(tag, %{field: bad_value}, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}),
|
||||
do: tag |> Tag.update_changeset(attrs) |> Repo.update()
|
||||
|
||||
@doc """
|
||||
Deletes a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_tag(tag, %User{id: 123})
|
||||
{:ok, %Tag{}}
|
||||
|
||||
iex> delete_tag(tag, %User{id: 123})
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()}
|
||||
def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete()
|
||||
|
||||
@doc """
|
||||
Deletes a tag.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_tag!(tag, %User{id: 123})
|
||||
%Tag{}
|
||||
|
||||
"""
|
||||
@spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
|
||||
def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete!()
|
||||
end
|
@ -44,8 +44,7 @@ defmodule CanneryWeb do
|
||||
|
||||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {CanneryWeb.LayoutView, "live.html"}
|
||||
use Phoenix.LiveView, layout: {CanneryWeb.LayoutView, :live}
|
||||
|
||||
on_mount CanneryWeb.InitAssigns
|
||||
unquote(view_helpers())
|
||||
@ -94,7 +93,7 @@ defmodule CanneryWeb do
|
||||
|
||||
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import CanneryWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers}
|
||||
import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers}
|
||||
import Phoenix.{Component, View}
|
||||
|
||||
alias CanneryWeb.Endpoint
|
||||
|
@ -5,6 +5,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
|
||||
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup}
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.LiveView.{JS, Socket}
|
||||
|
||||
@impl true
|
||||
@ -18,7 +19,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{ammo_group: ammo_group, current_user: current_user} = assigns, socket) do
|
||||
changeset =
|
||||
%ShotGroup{date: NaiveDateTime.utc_now(), count: 1}
|
||||
%ShotGroup{date: Date.utc_today()}
|
||||
|> ShotGroup.create_changeset(current_user, ammo_group, %{})
|
||||
|
||||
{:ok, socket |> assign(assigns) |> assign(:changeset, changeset)}
|
||||
@ -32,10 +33,13 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
|
||||
) do
|
||||
params = shot_group_params |> process_params(ammo_group)
|
||||
|
||||
changeset = %ShotGroup{} |> ShotGroup.create_changeset(current_user, ammo_group, params)
|
||||
|
||||
changeset =
|
||||
%ShotGroup{}
|
||||
|> ShotGroup.create_changeset(current_user, ammo_group, params)
|
||||
|> Map.put(:action, :validate)
|
||||
case changeset |> Changeset.apply_action(:validate) do
|
||||
{:ok, _data} -> changeset
|
||||
{:error, changeset} -> changeset
|
||||
end
|
||||
|
||||
{:noreply, socket |> assign(:changeset, changeset)}
|
||||
end
|
||||
@ -56,7 +60,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
|
||||
prompt = dgettext("prompts", "Shots recorded successfully")
|
||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
socket |> assign(changeset: changeset)
|
||||
end
|
||||
|
||||
@ -65,14 +69,14 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
|
||||
|
||||
# calculate count from shots left
|
||||
defp process_params(params, %AmmoGroup{count: count}) do
|
||||
new_count =
|
||||
if params |> Map.get("ammo_left", "0") == "" do
|
||||
"0"
|
||||
shot_group_count =
|
||||
if params |> Map.get("ammo_left", "") == "" do
|
||||
nil
|
||||
else
|
||||
params |> Map.get("ammo_left", "0")
|
||||
new_count = params |> Map.get("ammo_left") |> String.to_integer()
|
||||
count - new_count
|
||||
end
|
||||
|> String.to_integer()
|
||||
|
||||
params |> Map.put("count", count - new_count)
|
||||
params |> Map.put("count", shot_group_count)
|
||||
end
|
||||
end
|
||||
|
@ -37,9 +37,12 @@
|
||||
|
||||
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :notes,
|
||||
id: "add-shot-group-form-notes",
|
||||
class: "input input-primary col-span-2",
|
||||
placeholder: "Really great weather",
|
||||
phx_hook: "MaintainAttrs"
|
||||
maxlength: 255,
|
||||
placeholder: gettext("Really great weather"),
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :notes, "col-span-3") %>
|
||||
|
||||
|
@ -1,103 +0,0 @@
|
||||
defmodule CanneryWeb.Components.AmmoGroupCard do
|
||||
@moduledoc """
|
||||
Display card for an ammo group
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
alias Cannery.{Ammo, Ammo.AmmoGroup, Repo}
|
||||
alias CanneryWeb.Endpoint
|
||||
|
||||
attr :ammo_group, AmmoGroup, required: true
|
||||
attr :show_container, :boolean, default: false
|
||||
slot(:inner_block)
|
||||
|
||||
def ammo_group_card(%{ammo_group: ammo_group} = assigns) do
|
||||
assigns =
|
||||
%{show_container: show_container} = assigns |> assign_new(:show_container, fn -> false end)
|
||||
|
||||
preloads = if show_container, do: [:ammo_type, :container], else: [:ammo_type]
|
||||
ammo_group = ammo_group |> Repo.preload(preloads)
|
||||
|
||||
assigns = assigns |> assign(:ammo_group, ammo_group)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id={"ammo_group-#{@ammo_group.id}"}
|
||||
class="mx-4 my-2 px-8 py-4
|
||||
flex flex-col justify-center items-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link">
|
||||
<h1 class="title text-xl title-primary-500">
|
||||
<%= @ammo_group.ammo_type.name %>
|
||||
</h1>
|
||||
</.link>
|
||||
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Count:") %>
|
||||
<%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:if={@ammo_group |> Ammo.get_original_count() != @ammo_group.count}
|
||||
class="rounded-lg title text-lg"
|
||||
>
|
||||
<%= gettext("Original Count:") %>
|
||||
<%= @ammo_group |> Ammo.get_original_count() %>
|
||||
</span>
|
||||
|
||||
<span :if={@ammo_group.notes} class="rounded-lg title text-lg">
|
||||
<%= gettext("Notes:") %>
|
||||
<%= @ammo_group.notes %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Purchased on:") %>
|
||||
<.date date={@ammo_group.purchased_on} />
|
||||
</span>
|
||||
|
||||
<span :if={@ammo_group |> Ammo.get_last_used_shot_group()} class="rounded-lg title text-lg">
|
||||
<%= gettext("Last used on:") %>
|
||||
<.date date={@ammo_group |> Ammo.get_last_used_shot_group() |> Map.get(:date)} />
|
||||
</span>
|
||||
|
||||
<%= if @ammo_group.price_paid do %>
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Price paid:") %>
|
||||
<%= gettext("$%{amount}",
|
||||
amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
|
||||
) %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("CPR:") %>
|
||||
<%= gettext("$%{amount}",
|
||||
amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
|
||||
) %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<span :if={@show_container and @ammo_group.container} class="rounded-lg title text-lg">
|
||||
<%= gettext("Container:") %>
|
||||
|
||||
<.link
|
||||
navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container)}
|
||||
class="link"
|
||||
>
|
||||
<%= @ammo_group.container.name %>
|
||||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={assigns |> Map.has_key?(:inner_block)}
|
||||
class="mt-4 flex space-x-4 justify-center items-center"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
@ -3,7 +3,9 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
A component that displays a list of ammo groups
|
||||
"""
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Repo}
|
||||
alias Cannery.{Accounts.User, Ammo.AmmoGroup, ComparableDate}
|
||||
alias Cannery.{ActivityLog, Ammo, Containers}
|
||||
alias CanneryWeb.Components.TableComponent
|
||||
alias Ecto.UUID
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@ -13,6 +15,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
required(:id) => UUID.t(),
|
||||
required(:current_user) => User.t(),
|
||||
required(:ammo_groups) => [AmmoGroup.t()],
|
||||
required(:show_used) => boolean(),
|
||||
optional(:ammo_type) => Rendered.t(),
|
||||
optional(:range) => Rendered.t(),
|
||||
optional(:container) => Rendered.t(),
|
||||
@ -21,7 +24,11 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
},
|
||||
Socket.t()
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{id: _id, ammo_groups: _ammo_group, current_user: _current_user} = assigns, socket) do
|
||||
def update(
|
||||
%{id: _id, ammo_groups: _ammo_group, current_user: _current_user, show_used: _show_used} =
|
||||
assigns,
|
||||
socket
|
||||
) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
@ -42,65 +49,75 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
ammo_type: ammo_type,
|
||||
range: range,
|
||||
container: container,
|
||||
actions: actions
|
||||
actions: actions,
|
||||
show_used: show_used
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] do
|
||||
[]
|
||||
else
|
||||
[%{label: nil, key: :actions, sortable: false}]
|
||||
end
|
||||
|
||||
columns = [
|
||||
%{label: gettext("Purchased on"), key: :purchased_on},
|
||||
%{label: gettext("Last used on"), key: :used_up_on} | 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("Count"), key: :count},
|
||||
%{label: gettext("Original Count"), key: :original_count},
|
||||
%{label: gettext("Price paid"), key: :price_paid},
|
||||
%{label: gettext("CPR"), key: :cpr},
|
||||
|> 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},
|
||||
%{label: gettext("Notes"), key: :notes}
|
||||
| columns
|
||||
]
|
||||
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 != []
|
||||
)
|
||||
|
||||
columns =
|
||||
if ammo_type == [] do
|
||||
columns
|
||||
else
|
||||
[%{label: gettext("Ammo type"), key: :ammo_type} | columns]
|
||||
end
|
||||
containers =
|
||||
ammo_groups
|
||||
|> Enum.map(fn %{container_id: container_id} -> container_id end)
|
||||
|> Containers.get_containers(current_user)
|
||||
|
||||
extra_data = %{
|
||||
current_user: current_user,
|
||||
ammo_type: ammo_type,
|
||||
columns: columns,
|
||||
container: container,
|
||||
containers: containers,
|
||||
original_counts: Ammo.get_original_counts(ammo_groups, current_user),
|
||||
cprs: Ammo.get_cprs(ammo_groups, current_user),
|
||||
last_used_dates: ActivityLog.get_last_used_dates(ammo_groups, current_user),
|
||||
percentages_remaining: Ammo.get_percentages_remaining(ammo_groups, current_user),
|
||||
actions: actions,
|
||||
range: range
|
||||
}
|
||||
|
||||
rows =
|
||||
ammo_groups
|
||||
|> Repo.preload([:ammo_type, :container])
|
||||
|> Enum.map(fn ammo_group ->
|
||||
ammo_group |> get_row_data_for_ammo_group(extra_data)
|
||||
end)
|
||||
@ -112,20 +129,13 @@ 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
|
||||
|
||||
@spec get_row_data_for_ammo_group(AmmoGroup.t(), additional_data :: map()) :: map()
|
||||
defp get_row_data_for_ammo_group(ammo_group, %{columns: columns} = additional_data) do
|
||||
ammo_group = ammo_group |> Repo.preload([:ammo_type, :container])
|
||||
|
||||
columns
|
||||
|> Map.new(fn %{key: key} ->
|
||||
{key, get_value_for_key(key, ammo_group, additional_data)}
|
||||
@ -147,33 +157,27 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:price_paid, %{price_paid: nil}, _additional_data), do: {"", nil}
|
||||
defp get_value_for_key(:price_paid, %{price_paid: nil}, _additional_data),
|
||||
do: {0, gettext("No cost information")}
|
||||
|
||||
defp get_value_for_key(:price_paid, %{price_paid: price_paid}, _additional_data),
|
||||
do: gettext("$%{amount}", amount: price_paid |> :erlang.float_to_binary(decimals: 2))
|
||||
|
||||
defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on}, _additional_data) do
|
||||
assigns = %{purchased_on: purchased_on}
|
||||
do: {price_paid, gettext("$%{amount}", amount: display_currency(price_paid))}
|
||||
|
||||
defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on} = assigns, _additional_data) do
|
||||
{purchased_on,
|
||||
~H"""
|
||||
<.date date={@purchased_on} />
|
||||
<.date id={"#{@id}-purchased-on"} date={@purchased_on} />
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:used_up_on, ammo_group, _additional_data) do
|
||||
last_shot_group_date =
|
||||
case ammo_group |> Ammo.get_last_used_shot_group() do
|
||||
%{date: last_shot_group_date} -> last_shot_group_date
|
||||
_no_shot_groups -> nil
|
||||
end
|
||||
defp get_value_for_key(:used_up_on, %{id: ammo_group_id}, %{last_used_dates: last_used_dates}) do
|
||||
last_used_date = last_used_dates |> Map.get(ammo_group_id)
|
||||
assigns = %{id: ammo_group_id, last_used_date: last_used_date}
|
||||
|
||||
assigns = %{last_shot_group_date: last_shot_group_date}
|
||||
|
||||
{last_shot_group_date,
|
||||
{last_used_date,
|
||||
~H"""
|
||||
<%= if @last_shot_group_date do %>
|
||||
<.date date={@last_shot_group_date} />
|
||||
<%= if @last_used_date do %>
|
||||
<.date id={"#{@id}-last-used-date"} date={@last_used_date} />
|
||||
<% else %>
|
||||
<%= gettext("Never used") %>
|
||||
<% end %>
|
||||
@ -189,8 +193,14 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:remaining, ammo_group, _additional_data),
|
||||
do: gettext("%{percentage}%", percentage: ammo_group |> Ammo.get_percentage_remaining())
|
||||
defp get_value_for_key(
|
||||
:remaining,
|
||||
%{id: ammo_group_id},
|
||||
%{percentages_remaining: percentages_remaining}
|
||||
) do
|
||||
percentage = Map.fetch!(percentages_remaining, ammo_group_id)
|
||||
{percentage, gettext("%{percentage}%", percentage: percentage)}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:actions, ammo_group, %{actions: actions}) do
|
||||
assigns = %{actions: actions, ammo_group: ammo_group}
|
||||
@ -204,31 +214,44 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do
|
||||
|
||||
defp get_value_for_key(
|
||||
:container,
|
||||
%{container: %{name: container_name}} = ammo_group,
|
||||
%{container: container}
|
||||
%{container_id: container_id} = ammo_group,
|
||||
%{container: container_block, containers: containers}
|
||||
) do
|
||||
assigns = %{container: container, ammo_group: ammo_group}
|
||||
container = %{name: container_name} = Map.fetch!(containers, container_id)
|
||||
|
||||
assigns = %{
|
||||
container: container,
|
||||
container_block: container_block,
|
||||
ammo_group: ammo_group
|
||||
}
|
||||
|
||||
{container_name,
|
||||
~H"""
|
||||
<%= render_slot(@container, @ammo_group) %>
|
||||
<%= render_slot(@container_block, {@ammo_group, @container}) %>
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:original_count, ammo_group, _additional_data),
|
||||
do: ammo_group |> Ammo.get_original_count()
|
||||
defp get_value_for_key(
|
||||
:original_count,
|
||||
%{id: ammo_group_id},
|
||||
%{original_counts: original_counts}
|
||||
) do
|
||||
Map.fetch!(original_counts, ammo_group_id)
|
||||
end
|
||||
|
||||
defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data),
|
||||
do: gettext("No cost information")
|
||||
do: {0, gettext("No cost information")}
|
||||
|
||||
defp get_value_for_key(:cpr, ammo_group, _additional_data) do
|
||||
gettext("$%{amount}",
|
||||
amount: ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
|
||||
)
|
||||
defp get_value_for_key(:cpr, %{id: ammo_group_id}, %{cprs: cprs}) do
|
||||
amount = Map.fetch!(cprs, ammo_group_id)
|
||||
{amount, gettext("$%{amount}", amount: display_currency(amount))}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:count, %{count: count}, _additional_data),
|
||||
do: if(count == 0, do: gettext("Empty"), else: count)
|
||||
do: if(count == 0, do: {0, gettext("Empty")}, else: count)
|
||||
|
||||
defp get_value_for_key(key, ammo_group, _additional_data), do: ammo_group |> Map.get(key)
|
||||
|
||||
@spec display_currency(float()) :: String.t()
|
||||
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
|
||||
end
|
||||
|
@ -3,7 +3,8 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
|
||||
A component that displays a list of ammo type
|
||||
"""
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoType}
|
||||
alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoType}
|
||||
alias 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,92 +39,146 @@ 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)
|
||||
|> Enum.any?(fn ammo_type -> Map.get(ammo_type, key, default_value) != default_value end)
|
||||
end)
|
||||
end)
|
||||
|> Kernel.++([
|
||||
%{label: gettext("Rounds"), key: :round_count, type: :round_count}
|
||||
])
|
||||
|> Kernel.++(
|
||||
if show_used do
|
||||
[
|
||||
|
||||
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
|
||||
)
|
||||
|> TableComponent.maybe_compose_columns(
|
||||
%{
|
||||
label: gettext("Used packs"),
|
||||
key: :used_pack_count,
|
||||
type: :used_pack_count
|
||||
},
|
||||
show_used
|
||||
)
|
||||
|> 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
|
||||
},
|
||||
%{
|
||||
label: gettext("Total ever rounds"),
|
||||
key: :historical_round_count,
|
||||
type: :historical_round_count
|
||||
}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
show_used
|
||||
)
|
||||
|> Kernel.++([%{label: gettext("Packs"), key: :ammo_count, type: :ammo_count}])
|
||||
|> Kernel.++(
|
||||
|> 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)
|
||||
average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user)
|
||||
|
||||
[used_counts, historical_round_counts, historical_pack_counts, used_pack_counts] =
|
||||
if show_used do
|
||||
[
|
||||
%{
|
||||
label: gettext("Used packs"),
|
||||
key: :used_ammo_count,
|
||||
type: :used_ammo_count
|
||||
},
|
||||
%{
|
||||
label: gettext("Total ever packs"),
|
||||
key: :historical_ammo_count,
|
||||
type: :historical_ammo_count
|
||||
}
|
||||
ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user),
|
||||
ammo_types |> Ammo.get_historical_count_for_ammo_types(current_user),
|
||||
ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true),
|
||||
ammo_types |> Ammo.get_used_ammo_groups_count_for_types(current_user)
|
||||
]
|
||||
else
|
||||
[]
|
||||
[nil, nil, nil, nil]
|
||||
end
|
||||
)
|
||||
|> Kernel.++([
|
||||
%{label: gettext("Average CPR"), key: :avg_price_paid, type: :avg_price_paid},
|
||||
%{label: nil, key: "actions", type: :actions, sortable: false}
|
||||
])
|
||||
|
||||
extra_data = %{actions: actions, current_user: current_user}
|
||||
extra_data = %{
|
||||
actions: actions,
|
||||
current_user: current_user,
|
||||
used_counts: used_counts,
|
||||
round_counts: round_counts,
|
||||
historical_round_counts: historical_round_counts,
|
||||
packs_count: packs_count,
|
||||
used_pack_counts: used_pack_counts,
|
||||
historical_pack_counts: historical_pack_counts,
|
||||
average_costs: average_costs
|
||||
}
|
||||
|
||||
rows =
|
||||
ammo_types
|
||||
@ -136,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
|
||||
@ -153,46 +205,72 @@ 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, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
|
||||
defp get_ammo_type_value(:round_count, _key, %{id: ammo_type_id}, %{round_counts: round_counts}),
|
||||
do: Map.get(round_counts, ammo_type_id, 0)
|
||||
|
||||
defp get_ammo_type_value(:historical_round_count, _key, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_historical_count_for_ammo_type(current_user)
|
||||
defp get_ammo_type_value(
|
||||
:historical_round_count,
|
||||
_key,
|
||||
%{id: ammo_type_id},
|
||||
%{historical_round_counts: historical_round_counts}
|
||||
) do
|
||||
Map.get(historical_round_counts, ammo_type_id, 0)
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:used_round_count, _key, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_used_count_for_ammo_type(current_user)
|
||||
defp get_ammo_type_value(
|
||||
:used_round_count,
|
||||
_key,
|
||||
%{id: ammo_type_id},
|
||||
%{used_counts: used_counts}
|
||||
) do
|
||||
Map.get(used_counts, ammo_type_id, 0)
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:historical_ammo_count, _key, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true)
|
||||
defp get_ammo_type_value(
|
||||
:historical_pack_count,
|
||||
_key,
|
||||
%{id: ammo_type_id},
|
||||
%{historical_pack_counts: historical_pack_counts}
|
||||
) do
|
||||
Map.get(historical_pack_counts, ammo_type_id, 0)
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:used_ammo_count, _key, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_used_ammo_groups_count_for_type(current_user)
|
||||
defp get_ammo_type_value(
|
||||
:used_pack_count,
|
||||
_key,
|
||||
%{id: ammo_type_id},
|
||||
%{used_pack_counts: used_pack_counts}
|
||||
) do
|
||||
Map.get(used_pack_counts, ammo_type_id, 0)
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:ammo_count, _key, ammo_type, %{current_user: current_user}),
|
||||
do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user)
|
||||
defp get_ammo_type_value(:ammo_count, _key, %{id: ammo_type_id}, %{packs_count: packs_count}),
|
||||
do: Map.get(packs_count, ammo_type_id)
|
||||
|
||||
defp get_ammo_type_value(:avg_price_paid, _key, ammo_type, %{current_user: current_user}) do
|
||||
case ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user) do
|
||||
nil -> gettext("No cost information")
|
||||
count -> gettext("$%{amount}", amount: count |> :erlang.float_to_binary(decimals: 2))
|
||||
defp get_ammo_type_value(
|
||||
:avg_price_paid,
|
||||
_key,
|
||||
%{id: ammo_type_id},
|
||||
%{average_costs: average_costs}
|
||||
) do
|
||||
case Map.get(average_costs, ammo_type_id) do
|
||||
nil -> {0, gettext("No cost information")}
|
||||
count -> {count, gettext("$%{amount}", amount: display_currency(count))}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:name, _key, ammo_type, _other_data) do
|
||||
defp get_ammo_type_value(:name, _key, %{name: ammo_type_name} = ammo_type, _other_data) do
|
||||
assigns = %{ammo_type: ammo_type}
|
||||
|
||||
{ammo_type_name,
|
||||
~H"""
|
||||
<.link
|
||||
navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_type)}
|
||||
class="link"
|
||||
data-qa={"view-name-#{@ammo_type.id}"}
|
||||
>
|
||||
<.link navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_type)} class="link">
|
||||
<%= @ammo_type.name %>
|
||||
</.link>
|
||||
"""
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_ammo_type_value(:actions, _key, ammo_type, %{actions: actions}) do
|
||||
@ -206,4 +284,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do
|
||||
defp get_ammo_type_value(nil, _key, _ammo_type, _other_data), do: nil
|
||||
|
||||
defp get_ammo_type_value(_other, key, ammo_type, _other_data), do: ammo_type |> Map.get(key)
|
||||
|
||||
@spec display_currency(float()) :: String.t()
|
||||
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
|
||||
end
|
||||
|
@ -1,81 +0,0 @@
|
||||
defmodule CanneryWeb.Components.ContainerCard do
|
||||
@moduledoc """
|
||||
Display card for a container
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
import CanneryWeb.Components.TagCard
|
||||
alias Cannery.{Containers, Containers.Container, Repo}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias Phoenix.LiveView.Rendered
|
||||
|
||||
attr :container, Container, required: true
|
||||
slot(:tag_actions)
|
||||
slot(:inner_block)
|
||||
|
||||
@spec container_card(assigns :: map()) :: Rendered.t()
|
||||
def container_card(%{container: container} = assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(container: container |> Repo.preload([:tags, :ammo_groups]))
|
||||
|> assign_new(:tag_actions, fn -> [] end)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id={"container-#{@container.id}"}
|
||||
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
|
||||
flex flex-col justify-center items-center space-y-4
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
|
||||
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl">
|
||||
<%= @container.name %>
|
||||
</h1>
|
||||
</.link>
|
||||
|
||||
<span :if={@container.desc} class="rounded-lg title text-lg">
|
||||
<%= gettext("Description:") %>
|
||||
<%= @container.desc %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Type:") %>
|
||||
<%= @container.type %>
|
||||
</span>
|
||||
|
||||
<span :if={@container.location} class="rounded-lg title text-lg">
|
||||
<%= gettext("Location:") %>
|
||||
<%= @container.location %>
|
||||
</span>
|
||||
|
||||
<%= unless @container.ammo_groups |> Enum.empty?() do %>
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Packs:") %>
|
||||
<%= @container |> Containers.get_container_ammo_group_count!() %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Rounds:") %>
|
||||
<%= @container |> Containers.get_container_rounds!() %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<div class="flex flex-wrap justify-center items-center">
|
||||
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
|
||||
|
||||
<%= render_slot(@tag_actions) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={assigns |> Map.has_key?(:inner_block)}
|
||||
class="flex space-x-4 justify-center items-center"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
@ -3,8 +3,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
||||
A component that displays a list of containers
|
||||
"""
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, Containers, Containers.Container, Repo}
|
||||
alias CanneryWeb.Components.TagCard
|
||||
alias Cannery.{Accounts.User, Ammo, Containers.Container}
|
||||
alias Ecto.UUID
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@ -46,11 +45,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
||||
%{label: gettext("Name"), key: :name, type: :string},
|
||||
%{label: gettext("Description"), key: :desc, type: :string},
|
||||
%{label: gettext("Location"), key: :location, type: :string},
|
||||
%{label: gettext("Type"), key: :type, type: :string},
|
||||
%{label: gettext("Packs"), key: :packs, type: :integer},
|
||||
%{label: gettext("Rounds"), key: :rounds, type: :string},
|
||||
%{label: gettext("Tags"), key: :tags, type: :tags},
|
||||
%{label: nil, key: :actions, sortable: false, type: :actions}
|
||||
%{label: gettext("Type"), key: :type, type: :string}
|
||||
]
|
||||
|> Enum.filter(fn %{key: key, type: type} ->
|
||||
# remove columns if all values match defaults
|
||||
@ -65,11 +60,19 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
||||
type in [:tags, :actions] or not (container |> Map.get(key) == default_value)
|
||||
end)
|
||||
end)
|
||||
|> Enum.concat([
|
||||
%{label: gettext("Packs"), key: :packs, type: :integer},
|
||||
%{label: gettext("Rounds"), key: :rounds, type: :integer},
|
||||
%{label: gettext("Tags"), key: :tags, type: :tags},
|
||||
%{label: gettext("Actions"), key: :actions, sortable: false, type: :actions}
|
||||
])
|
||||
|
||||
extra_data = %{
|
||||
current_user: current_user,
|
||||
tag_actions: tag_actions,
|
||||
actions: actions
|
||||
actions: actions,
|
||||
pack_count: Ammo.get_ammo_groups_count_for_containers(containers, current_user),
|
||||
round_count: Ammo.get_round_count_for_containers(containers, current_user)
|
||||
}
|
||||
|
||||
rows =
|
||||
@ -101,8 +104,6 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
||||
|
||||
@spec get_row_data_for_container(Container.t(), columns :: [map()], extra_data :: map) :: map()
|
||||
defp get_row_data_for_container(container, columns, extra_data) do
|
||||
container = container |> Repo.preload([:ammo_groups, :tags])
|
||||
|
||||
columns
|
||||
|> Map.new(fn %{key: key} -> {key, get_value_for_key(key, container, extra_data)} end)
|
||||
end
|
||||
@ -121,21 +122,27 @@ defmodule CanneryWeb.Components.ContainerTableComponent do
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:packs, container, _extra_data) do
|
||||
container |> Containers.get_container_ammo_group_count!()
|
||||
defp get_value_for_key(:packs, %{id: container_id}, %{pack_count: pack_count}) do
|
||||
pack_count |> Map.get(container_id, 0)
|
||||
end
|
||||
|
||||
defp get_value_for_key(:rounds, container, _extra_data) do
|
||||
container |> Containers.get_container_rounds!()
|
||||
defp get_value_for_key(:rounds, %{id: container_id}, %{round_count: round_count}) do
|
||||
round_count |> Map.get(container_id, 0)
|
||||
end
|
||||
|
||||
defp get_value_for_key(:tags, container, %{tag_actions: tag_actions}) do
|
||||
assigns = %{tag_actions: tag_actions, container: container}
|
||||
|
||||
{container.tags |> Enum.map(fn %{name: name} -> name end),
|
||||
tag_names =
|
||||
container.tags
|
||||
|> Enum.map(fn %{name: name} -> name end)
|
||||
|> Enum.sort()
|
||||
|> Enum.join(" ")
|
||||
|
||||
{tag_names,
|
||||
~H"""
|
||||
<div class="flex flex-wrap justify-center items-center">
|
||||
<TagCard.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
|
||||
<.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
|
||||
|
||||
<%= render_slot(@tag_actions, @container) %>
|
||||
</div>
|
||||
|
149
lib/cannery_web/components/core_components.ex
Normal file
149
lib/cannery_web/components/core_components.ex
Normal file
@ -0,0 +1,149 @@
|
||||
defmodule CanneryWeb.CoreComponents do
|
||||
@moduledoc """
|
||||
Provides core UI components.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
import CanneryWeb.{Gettext, ViewHelpers}
|
||||
alias Cannery.{Accounts, Accounts.Invite, Accounts.User}
|
||||
alias Cannery.{Ammo, Ammo.AmmoGroup}
|
||||
alias Cannery.{Containers.Container, Containers.Tag}
|
||||
alias CanneryWeb.{Endpoint, HomeLive}
|
||||
alias CanneryWeb.Router.Helpers, as: Routes
|
||||
alias Phoenix.LiveView.{JS, Rendered}
|
||||
|
||||
embed_templates "core_components/*"
|
||||
|
||||
attr :title_content, :string, default: nil
|
||||
attr :current_user, User, default: nil
|
||||
|
||||
def topbar(assigns)
|
||||
|
||||
attr :return_to, :string, required: true
|
||||
slot(:inner_block)
|
||||
|
||||
@doc """
|
||||
Renders a live component inside a modal.
|
||||
|
||||
The rendered modal receives a `:return_to` option to properly update
|
||||
the URL when the modal is closed.
|
||||
|
||||
## Examples
|
||||
|
||||
<.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
|
||||
id={@<%= schema.singular %>.id || :new}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}
|
||||
<%= schema.singular %>: @<%= schema.singular %>
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
def modal(assigns)
|
||||
|
||||
defp hide_modal(js \\ %JS{}) do
|
||||
js
|
||||
|> JS.hide(to: "#modal", transition: "fade-out")
|
||||
|> JS.hide(to: "#modal-bg", transition: "fade-out")
|
||||
|> JS.hide(to: "#modal-content", transition: "fade-out-scale")
|
||||
end
|
||||
|
||||
attr :action, :string, required: true
|
||||
attr :value, :boolean, required: true
|
||||
attr :id, :string, default: nil
|
||||
slot(:inner_block)
|
||||
|
||||
@doc """
|
||||
A toggle button element that can be directed to a liveview or a
|
||||
live_component's `handle_event/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.toggle_button action="my_liveview_action" value={@some_value}>
|
||||
<span>Toggle me!</span>
|
||||
</.toggle_button>
|
||||
<.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
|
||||
<span>Whatever you want</span>
|
||||
</.toggle_button>
|
||||
"""
|
||||
def toggle_button(assigns)
|
||||
|
||||
attr :container, Container, required: true
|
||||
attr :current_user, User, required: true
|
||||
slot(:tag_actions)
|
||||
slot(:inner_block)
|
||||
|
||||
@spec container_card(assigns :: map()) :: Rendered.t()
|
||||
def container_card(assigns)
|
||||
|
||||
attr :tag, Tag, required: true
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def tag_card(assigns)
|
||||
|
||||
attr :tag, Tag, required: true
|
||||
|
||||
def simple_tag_card(assigns)
|
||||
|
||||
attr :ammo_group, AmmoGroup, required: true
|
||||
attr :current_user, User, required: true
|
||||
attr :original_count, :integer, default: nil
|
||||
attr :cpr, :integer, default: nil
|
||||
attr :last_used_date, Date, default: nil
|
||||
attr :container, Container, default: nil
|
||||
slot(:inner_block)
|
||||
|
||||
def ammo_group_card(assigns)
|
||||
|
||||
@spec display_currency(float()) :: String.t()
|
||||
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
|
||||
|
||||
attr :user, User, required: true
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def user_card(assigns)
|
||||
|
||||
attr :invite, Invite, required: true
|
||||
attr :use_count, :integer, default: nil
|
||||
attr :current_user, User, required: true
|
||||
slot(:inner_block)
|
||||
slot(:code_actions)
|
||||
|
||||
def invite_card(assigns)
|
||||
|
||||
attr :content, :string, required: true
|
||||
attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
|
||||
attr :image_class, :string, default: "w-64 h-max"
|
||||
attr :width, :integer, default: 384, doc: "width of png to generate"
|
||||
|
||||
@doc """
|
||||
Creates a downloadable QR Code element
|
||||
"""
|
||||
def qr_code(assigns)
|
||||
|
||||
attr :id, :string, required: true
|
||||
attr :date, :any, required: true, doc: "A `Date` struct or nil"
|
||||
|
||||
@doc """
|
||||
Phoenix.Component for a <date> element that renders the Date in the user's
|
||||
local timezone
|
||||
"""
|
||||
def date(assigns)
|
||||
|
||||
attr :id, :string, required: true
|
||||
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
|
||||
|
||||
@doc """
|
||||
Phoenix.Component for a <time> element that renders the naivedatetime in the
|
||||
user's local timezone
|
||||
"""
|
||||
def datetime(assigns)
|
||||
|
||||
@spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
|
||||
defp cast_datetime(%NaiveDateTime{} = datetime) do
|
||||
datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
|
||||
end
|
||||
|
||||
defp cast_datetime(_datetime), do: ""
|
||||
end
|
@ -0,0 +1,65 @@
|
||||
<div
|
||||
id={"ammo_group-#{@ammo_group.id}"}
|
||||
class="mx-4 my-2 px-8 py-4
|
||||
flex flex-col justify-center items-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link">
|
||||
<h1 class="title text-xl title-primary-500">
|
||||
<%= @ammo_group.ammo_type.name %>
|
||||
</h1>
|
||||
</.link>
|
||||
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Count:") %>
|
||||
<%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:if={@original_count && @original_count != @ammo_group.count}
|
||||
class="rounded-lg title text-lg"
|
||||
>
|
||||
<%= gettext("Original Count:") %>
|
||||
<%= @original_count %>
|
||||
</span>
|
||||
|
||||
<span :if={@ammo_group.notes} class="rounded-lg title text-lg">
|
||||
<%= gettext("Notes:") %>
|
||||
<%= @ammo_group.notes %>
|
||||
</span>
|
||||
|
||||
<span :if={@ammo_group.purchased_on} class="rounded-lg title text-lg">
|
||||
<%= gettext("Purchased on:") %>
|
||||
<.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} />
|
||||
</span>
|
||||
|
||||
<span :if={@last_used_date} class="rounded-lg title text-lg">
|
||||
<%= gettext("Last used on:") %>
|
||||
<.date id={"#{@ammo_group.id}-last-used-on"} date={@last_used_date} />
|
||||
</span>
|
||||
|
||||
<span :if={@ammo_group.price_paid} class="rounded-lg title text-lg">
|
||||
<%= gettext("Price paid:") %>
|
||||
<%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %>
|
||||
</span>
|
||||
|
||||
<span :if={@cpr} class="rounded-lg title text-lg">
|
||||
<%= gettext("CPR:") %>
|
||||
<%= gettext("$%{amount}", amount: display_currency(@cpr)) %>
|
||||
</span>
|
||||
|
||||
<span :if={@container} class="rounded-lg title text-lg">
|
||||
<%= gettext("Container:") %>
|
||||
|
||||
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
|
||||
<%= @container.name %>
|
||||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div :if={@inner_block} class="mt-4 flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,58 @@
|
||||
<div
|
||||
id={"container-#{@container.id}"}
|
||||
class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
|
||||
flex flex-col justify-around items-center space-y-4
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl">
|
||||
<%= @container.name %>
|
||||
</h1>
|
||||
</.link>
|
||||
|
||||
<div class="flex flex-col justify-center items-center space-y-2">
|
||||
<span :if={@container.desc} class="rounded-lg title text-lg">
|
||||
<%= gettext("Description:") %>
|
||||
<%= @container.desc %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Type:") %>
|
||||
<%= @container.type %>
|
||||
</span>
|
||||
|
||||
<span :if={@container.location} class="rounded-lg title text-lg">
|
||||
<%= gettext("Location:") %>
|
||||
<%= @container.location %>
|
||||
</span>
|
||||
|
||||
<%= if @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) != 0 do %>
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Packs:") %>
|
||||
<%= @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Rounds:") %>
|
||||
<%= @container |> Ammo.get_round_count_for_container!(@current_user) %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<div
|
||||
:if={@tag_actions || @container.tags != []}
|
||||
class="flex flex-wrap justify-center items-center"
|
||||
>
|
||||
<.simple_tag_card :for={tag <- @container.tags} tag={tag} />
|
||||
|
||||
<%= if @tag_actions, do: render_slot(@tag_actions) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={assigns |> Map.has_key?(:inner_block)}
|
||||
class="flex space-x-4 justify-center items-center"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
|
||||
<%= Date.to_iso8601(@date, :extended) %>
|
||||
</time>
|
@ -0,0 +1,3 @@
|
||||
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
|
||||
<%= cast_datetime(@datetime) %>
|
||||
</time>
|
@ -0,0 +1,46 @@
|
||||
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out">
|
||||
<h1 class="title text-xl">
|
||||
<%= @invite.name %>
|
||||
</h1>
|
||||
|
||||
<%= if @invite.disabled_at |> is_nil() do %>
|
||||
<h2 class="title text-md">
|
||||
<%= if @invite.uses_left do %>
|
||||
<%= gettext(
|
||||
"Uses Left: %{uses_left_count}",
|
||||
uses_left_count: @invite.uses_left
|
||||
) %>
|
||||
<% else %>
|
||||
<%= gettext("Uses Left: Unlimited") %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<% else %>
|
||||
<h2 class="title text-md">
|
||||
<%= gettext("Invite Disabled") %>
|
||||
</h2>
|
||||
<% end %>
|
||||
|
||||
<.qr_code
|
||||
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
|
||||
filename={@invite.name}
|
||||
/>
|
||||
|
||||
<h2 :if={@use_count && @use_count != 0} class="title text-md">
|
||||
<%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||
<code
|
||||
id={"code-#{@invite.id}"}
|
||||
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
|
||||
phx-no-format
|
||||
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
|
||||
<%= if @code_actions, do: render_slot(@code_actions) %>
|
||||
</div>
|
||||
|
||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
44
lib/cannery_web/components/core_components/modal.html.heex
Normal file
44
lib/cannery_web/components/core_components/modal.html.heex
Normal file
@ -0,0 +1,44 @@
|
||||
<.link
|
||||
patch={@return_to}
|
||||
id="modal-bg"
|
||||
class="fade-in fixed z-10 left-0 top-0
|
||||
w-full h-full overflow-hidden
|
||||
p-8 flex flex-col justify-center items-center cursor-auto"
|
||||
style="background-color: rgba(0,0,0,0.4);"
|
||||
phx-remove={hide_modal()}
|
||||
aria-label={gettext("Close modal")}
|
||||
>
|
||||
<span class="hidden"></span>
|
||||
</.link>
|
||||
|
||||
<div
|
||||
id="modal"
|
||||
class="fixed z-10 left-0 top-0 pointer-events-none
|
||||
w-full h-full overflow-hidden
|
||||
p-4 sm:p-8 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div
|
||||
id="modal-content"
|
||||
class="fade-in-scale w-full max-w-3xl relative
|
||||
pointer-events-auto overflow-hidden
|
||||
px-8 py-4 sm:py-8
|
||||
flex flex-col justify-start items-center
|
||||
bg-white border-2 rounded-lg"
|
||||
>
|
||||
<.link
|
||||
patch={@return_to}
|
||||
id="close"
|
||||
class="absolute top-8 right-10
|
||||
text-gray-500 hover:text-gray-800
|
||||
transition-all duration-500 ease-in-out"
|
||||
phx-remove={hide_modal()}
|
||||
aria-label={gettext("Close modal")}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-times"></i>
|
||||
</.link>
|
||||
|
||||
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
<a href={qr_code_image(@content)} download={@filename <> ".png"}>
|
||||
<img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
|
||||
</a>
|
@ -0,0 +1,6 @@
|
||||
<h1
|
||||
class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
|
||||
style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
|
||||
>
|
||||
<%= @tag.name %>
|
||||
</h1>
|
@ -0,0 +1,9 @@
|
||||
<div
|
||||
id={"tag-#{@tag.id}"}
|
||||
class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<.simple_tag_card tag={@tag} />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
@ -0,0 +1,30 @@
|
||||
<label for={@id || @action} class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
id={@id || @action}
|
||||
type="checkbox"
|
||||
value={@value}
|
||||
checked={@value}
|
||||
class="sr-only peer"
|
||||
aria-labelledby={"#{@id || @action}-label"}
|
||||
{
|
||||
if assigns |> Map.has_key?(:target),
|
||||
do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
|
||||
else: %{"phx-click": @action, "phx-value-value": @value}
|
||||
}
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-300 rounded-full peer
|
||||
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
|
||||
peer-checked:bg-gray-600
|
||||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||
after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
|
||||
after:border after:rounded-full after:h-5 after:w-5
|
||||
after:transition-all after:duration-250 after:ease-in-out
|
||||
transition-colors duration-250 ease-in-out">
|
||||
</div>
|
||||
<span
|
||||
id={"#{@id || @action}-label"}
|
||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 whitespace-nowrap"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</span>
|
||||
</label>
|
130
lib/cannery_web/components/core_components/topbar.html.heex
Normal file
130
lib/cannery_web/components/core_components/topbar.html.heex
Normal file
@ -0,0 +1,130 @@
|
||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-500">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
||||
<.link
|
||||
navigate={Routes.live_path(Endpoint, HomeLive)}
|
||||
class="inline mx-2 my-1 leading-5 text-xl text-white"
|
||||
>
|
||||
<img
|
||||
src={Routes.static_path(Endpoint, "/images/cannery.svg")}
|
||||
alt={gettext("Cannery logo")}
|
||||
class="inline-block h-8 mx-1"
|
||||
/>
|
||||
<h1 class="inline hover:underline">Cannery</h1>
|
||||
</.link>
|
||||
|
||||
<%= if @title_content do %>
|
||||
<span class="mx-2 my-1">
|
||||
|
|
||||
</span>
|
||||
<%= @title_content %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<hr class="mb-2 sm:hidden hr-light" />
|
||||
|
||||
<ul class="flex flex-row flex-wrap justify-center items-center
|
||||
text-lg text-white text-ellipsis">
|
||||
<%= if @current_user do %>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.tag_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Tags") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.container_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Containers") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.ammo_type_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Catalog") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.ammo_group_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Ammo") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.range_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Range") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.invite_index_path(Endpoint, :index)}
|
||||
class="text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Invites") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_settings_path(Endpoint, :edit)}
|
||||
class="text-white hover:underline truncate"
|
||||
>
|
||||
<%= @current_user.email %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_session_path(Endpoint, :delete)}
|
||||
method="delete"
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
|
||||
aria-label={gettext("Log out")}
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<li
|
||||
:if={
|
||||
@current_user |> Accounts.is_already_admin?() and
|
||||
function_exported?(Routes, :live_dashboard_path, 2)
|
||||
}
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
<.link
|
||||
navigate={Routes.live_dashboard_path(Endpoint, :home)}
|
||||
class="text-white hover:underline"
|
||||
aria-label={gettext("Live Dashboard")}
|
||||
>
|
||||
<i class="fas fa-gauge"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<% else %>
|
||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_registration_path(Endpoint, :new)}
|
||||
class="text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Register") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_session_path(Endpoint, :new)}
|
||||
class="text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Log in") %>
|
||||
</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
@ -0,0 +1,36 @@
|
||||
<div
|
||||
id={"user-#{@user.id}"}
|
||||
class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
||||
<%= @user.email %>
|
||||
</h1>
|
||||
|
||||
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
||||
<p>
|
||||
<%= if @user.confirmed_at do %>
|
||||
<%= gettext(
|
||||
"User was confirmed at%{confirmed_datetime}",
|
||||
confirmed_datetime: ""
|
||||
) %>
|
||||
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
|
||||
<% else %>
|
||||
<%= gettext("Email unconfirmed") %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= gettext(
|
||||
"User registered on%{registered_datetime}",
|
||||
registered_datetime: ""
|
||||
) %>
|
||||
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
@ -1,70 +0,0 @@
|
||||
defmodule CanneryWeb.Components.InviteCard do
|
||||
@moduledoc """
|
||||
Display card for an invite
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
alias Cannery.Accounts.{Invite, Invites, User}
|
||||
alias CanneryWeb.Endpoint
|
||||
|
||||
attr :invite, Invite, required: true
|
||||
attr :current_user, User, required: true
|
||||
slot(:inner_block)
|
||||
slot(:code_actions)
|
||||
|
||||
def invite_card(%{invite: invite, current_user: current_user} = assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:use_count, Invites.get_use_count(invite, current_user))
|
||||
|> assign_new(:code_actions, fn -> [] end)
|
||||
|
||||
~H"""
|
||||
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out">
|
||||
<h1 class="title text-xl">
|
||||
<%= @invite.name %>
|
||||
</h1>
|
||||
|
||||
<%= if @invite.disabled_at |> is_nil() do %>
|
||||
<h2 class="title text-md">
|
||||
<%= if @invite.uses_left do %>
|
||||
<%= gettext(
|
||||
"Uses Left: %{uses_left_count}",
|
||||
uses_left_count: @invite.uses_left
|
||||
) %>
|
||||
<% else %>
|
||||
<%= gettext("Uses Left: Unlimited") %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<% else %>
|
||||
<h2 class="title text-md">
|
||||
<%= gettext("Invite Disabled") %>
|
||||
</h2>
|
||||
<% end %>
|
||||
|
||||
<.qr_code
|
||||
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
|
||||
filename={@invite.name}
|
||||
/>
|
||||
|
||||
<h2 :if={@use_count != 0} class="title text-md">
|
||||
<%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||
<code
|
||||
id={"code-#{@invite.id}"}
|
||||
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
|
||||
phx-no-format
|
||||
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
|
||||
<%= render_slot(@code_actions) %>
|
||||
</div>
|
||||
|
||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
@ -6,6 +6,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Containers, Containers.Container}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@impl true
|
||||
@ -51,10 +52,9 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|
||||
|> case do
|
||||
{:ok, _ammo_group} ->
|
||||
prompt = dgettext("prompts", "Ammo moved to %{name} successfully", name: container_name)
|
||||
|
||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
socket |> assign(changeset: changeset)
|
||||
end
|
||||
|
||||
@ -64,10 +64,10 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|
||||
@impl true
|
||||
def render(%{containers: containers} = assigns) do
|
||||
columns = [
|
||||
%{label: gettext("Container"), key: "name"},
|
||||
%{label: gettext("Type"), key: "type"},
|
||||
%{label: gettext("Location"), key: "location"},
|
||||
%{label: nil, key: "actions", sortable: false}
|
||||
%{label: gettext("Container"), key: :name},
|
||||
%{label: gettext("Type"), key: :type},
|
||||
%{label: gettext("Location"), key: :location},
|
||||
%{label: gettext("Actions"), key: :actions, sortable: false}
|
||||
]
|
||||
|
||||
rows = containers |> get_rows_for_containers(assigns, columns)
|
||||
@ -110,8 +110,8 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|
||||
end)
|
||||
end
|
||||
|
||||
@spec get_row_value_by_key(String.t(), Container.t(), map()) :: any()
|
||||
defp get_row_value_by_key("actions", container, assigns) do
|
||||
@spec get_row_value_by_key(atom(), Container.t(), map()) :: any()
|
||||
defp get_row_value_by_key(:actions, container, assigns) do
|
||||
assigns = assigns |> Map.put(:container, container)
|
||||
|
||||
~H"""
|
||||
@ -129,6 +129,5 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_row_value_by_key(key, container, _assigns),
|
||||
do: container |> Map.get(key |> String.to_existing_atom())
|
||||
defp get_row_value_by_key(key, container, _assigns), do: container |> Map.get(key)
|
||||
end
|
||||
|
@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
|
||||
A component that displays a list of shot groups
|
||||
"""
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo}
|
||||
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo, ComparableDate}
|
||||
alias Ecto.UUID
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@ -41,11 +41,16 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
|
||||
%{label: gettext("Ammo"), key: :name},
|
||||
%{label: gettext("Rounds shot"), key: :count},
|
||||
%{label: gettext("Notes"), key: :notes},
|
||||
%{label: gettext("Date"), key: :date},
|
||||
%{label: nil, key: :actions, sortable: false}
|
||||
%{label: gettext("Date"), key: :date, type: ComparableDate},
|
||||
%{label: gettext("Actions"), key: :actions, sortable: false}
|
||||
]
|
||||
|
||||
extra_data = %{current_user: current_user, actions: actions}
|
||||
ammo_groups =
|
||||
shot_groups
|
||||
|> Enum.map(fn %{ammo_group_id: ammo_group_id} -> ammo_group_id end)
|
||||
|> Ammo.get_ammo_groups(current_user)
|
||||
|
||||
extra_data = %{current_user: current_user, actions: actions, ammo_groups: ammo_groups}
|
||||
|
||||
rows =
|
||||
shot_groups
|
||||
@ -79,34 +84,28 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do
|
||||
@spec get_row_data_for_shot_group(ShotGroup.t(), columns :: [map()], extra_data :: map()) ::
|
||||
map()
|
||||
defp get_row_data_for_shot_group(shot_group, columns, extra_data) do
|
||||
shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type)
|
||||
|
||||
columns
|
||||
|> Map.new(fn %{key: key} ->
|
||||
{key, get_row_value(key, shot_group, extra_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_row_value(
|
||||
:name,
|
||||
%{ammo_group: %{ammo_type: %{name: ammo_type_name} = ammo_group}},
|
||||
_extra_data
|
||||
) do
|
||||
assigns = %{ammo_group: ammo_group, ammo_type_name: ammo_type_name}
|
||||
defp get_row_value(:name, %{ammo_group_id: ammo_group_id}, %{ammo_groups: ammo_groups}) do
|
||||
assigns = %{ammo_group: ammo_group = Map.fetch!(ammo_groups, ammo_group_id)}
|
||||
|
||||
name_block = ~H"""
|
||||
{ammo_group.ammo_type.name,
|
||||
~H"""
|
||||
<.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="link">
|
||||
<%= @ammo_type_name %>
|
||||
<%= @ammo_group.ammo_type.name %>
|
||||
</.link>
|
||||
"""
|
||||
|
||||
{ammo_type_name, name_block}
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_row_value(:date, %{date: _date} = assigns, _extra_data) do
|
||||
defp get_row_value(:date, %{date: date} = assigns, _extra_data) do
|
||||
{date,
|
||||
~H"""
|
||||
<.date date={@date} />
|
||||
"""
|
||||
<.date id={"#{@id}-date"} date={@date} />
|
||||
"""}
|
||||
end
|
||||
|
||||
defp get_row_value(:actions, shot_group, %{actions: actions}) do
|
||||
|
@ -33,7 +33,8 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
optional(:class) => String.t(),
|
||||
optional(:row_class) => String.t(),
|
||||
optional(:alternate_row_class) => String.t(),
|
||||
optional(:sortable) => false
|
||||
optional(:sortable) => false,
|
||||
optional(:type) => module()
|
||||
}),
|
||||
required(:rows) =>
|
||||
list(%{
|
||||
@ -60,7 +61,8 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
:asc
|
||||
end
|
||||
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode)
|
||||
type = columns |> Enum.find(%{}, fn %{key: key} -> key == initial_key end) |> Map.get(:type)
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode, type)
|
||||
|
||||
socket =
|
||||
socket
|
||||
@ -68,6 +70,7 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
|> assign(
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
key: initial_key,
|
||||
last_sort_key: initial_key,
|
||||
sort_mode: initial_sort_mode
|
||||
)
|
||||
@ -81,7 +84,14 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
def handle_event(
|
||||
"sort_by",
|
||||
%{"sort-key" => key},
|
||||
%{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
|
||||
%{
|
||||
assigns: %{
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
last_sort_key: last_sort_key,
|
||||
sort_mode: sort_mode
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
key = key |> String.to_existing_atom()
|
||||
|
||||
@ -92,11 +102,28 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
{_new_sort_key, _last_sort_mode} -> :asc
|
||||
end
|
||||
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
|
||||
type =
|
||||
columns |> Enum.find(%{}, fn %{key: column_key} -> column_key == key end) |> Map.get(:type)
|
||||
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode, type)
|
||||
{:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
|
||||
end
|
||||
|
||||
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
|
||||
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
|
||||
when type in [Date, DateTime] do
|
||||
rows
|
||||
|> Enum.sort_by(
|
||||
fn row ->
|
||||
case row |> Map.get(key) do
|
||||
{custom_sort_key, _value} -> custom_sort_key
|
||||
value -> value
|
||||
end
|
||||
end,
|
||||
{sort_mode, type}
|
||||
)
|
||||
end
|
||||
|
||||
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, _type) do
|
||||
rows
|
||||
|> Enum.sort_by(
|
||||
fn row ->
|
||||
@ -108,4 +135,25 @@ defmodule CanneryWeb.Components.TableComponent do
|
||||
sort_mode
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Conditionally composes elements into the columns list, supports maps and
|
||||
lists. Works tail to front in order for efficiency
|
||||
|
||||
iex> []
|
||||
...> |> maybe_compose_columns(%{label: "Column 3"}, true)
|
||||
...> |> maybe_compose_columns(%{label: "Column 2"}, false)
|
||||
...> |> maybe_compose_columns(%{label: "Column 1"})
|
||||
[%{label: "Column 1"}, %{label: "Column 3"}]
|
||||
|
||||
"""
|
||||
@spec maybe_compose_columns(list(), element_to_add :: list() | map()) :: list()
|
||||
@spec maybe_compose_columns(list(), element_to_add :: list() | map(), boolean()) :: list()
|
||||
def maybe_compose_columns(columns, element_or_elements, add? \\ true)
|
||||
|
||||
def maybe_compose_columns(columns, elements, true) when is_list(elements),
|
||||
do: Enum.concat(elements, columns)
|
||||
|
||||
def maybe_compose_columns(columns, element, true) when is_map(element), do: [element | columns]
|
||||
def maybe_compose_columns(columns, _element_or_elements, false), do: columns
|
||||
end
|
||||
|
@ -1,38 +0,0 @@
|
||||
defmodule CanneryWeb.Components.TagCard do
|
||||
@moduledoc """
|
||||
Display card for a tag
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
alias Cannery.Tags.Tag
|
||||
|
||||
attr :tag, Tag, required: true
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def tag_card(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"tag-#{@tag.id}"}
|
||||
class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<.simple_tag_card tag={@tag} />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :tag, Tag, required: true
|
||||
|
||||
def simple_tag_card(assigns) do
|
||||
~H"""
|
||||
<h1
|
||||
class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
|
||||
style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
|
||||
>
|
||||
<%= @tag.name %>
|
||||
</h1>
|
||||
"""
|
||||
end
|
||||
end
|
@ -1,146 +0,0 @@
|
||||
defmodule CanneryWeb.Components.Topbar do
|
||||
@moduledoc """
|
||||
Component that renders a topbar with user functions/links
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
|
||||
alias Cannery.Accounts
|
||||
alias CanneryWeb.HomeLive
|
||||
|
||||
def topbar(assigns) do
|
||||
assigns =
|
||||
%{results: [], title_content: nil, flash: nil, current_user: nil} |> Map.merge(assigns)
|
||||
|
||||
~H"""
|
||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-400">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
||||
<.link
|
||||
navigate={Routes.live_path(Endpoint, HomeLive)}
|
||||
class="inline mx-2 my-1 leading-5 text-xl text-white"
|
||||
>
|
||||
<img
|
||||
src={Routes.static_path(Endpoint, "/images/cannery.svg")}
|
||||
alt={gettext("Cannery logo")}
|
||||
class="inline-block h-8 mx-1"
|
||||
/>
|
||||
<h1 class="inline hover:underline">Cannery</h1>
|
||||
</.link>
|
||||
|
||||
<%= if @title_content do %>
|
||||
<span class="mx-2 my-1">
|
||||
|
|
||||
</span>
|
||||
<%= @title_content %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<hr class="mb-2 sm:hidden hr-light" />
|
||||
|
||||
<ul class="flex flex-row flex-wrap justify-center items-center
|
||||
text-lg text-white text-ellipsis">
|
||||
<%= if @current_user do %>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.tag_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Tags") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.container_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Containers") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.ammo_type_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Catalog") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.ammo_group_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Ammo") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.range_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Range") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.invite_index_path(Endpoint, :index)}
|
||||
class="text-primary-600 text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Invites") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_settings_path(Endpoint, :edit)}
|
||||
class="text-primary-600 text-white hover:underline truncate"
|
||||
>
|
||||
<%= @current_user.email %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_session_path(Endpoint, :delete)}
|
||||
method="delete"
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<li
|
||||
:if={
|
||||
@current_user |> Accounts.is_already_admin?() and
|
||||
function_exported?(Routes, :live_dashboard_path, 2)
|
||||
}
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
<.link
|
||||
navigate={Routes.live_dashboard_path(Endpoint, :home)}
|
||||
class="text-white text-white hover:underline"
|
||||
>
|
||||
<i class="fas fa-gauge"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<% else %>
|
||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_registration_path(Endpoint, :new)}
|
||||
class="text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Register") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_session_path(Endpoint, :new)}
|
||||
class="text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Log in") %>
|
||||
</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
end
|
@ -1,52 +0,0 @@
|
||||
defmodule CanneryWeb.Components.UserCard do
|
||||
@moduledoc """
|
||||
Display card for a user
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
alias Cannery.Accounts.User
|
||||
|
||||
attr :user, User, required: true
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def user_card(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"user-#{@user.id}"}
|
||||
class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
||||
<%= @user.email %>
|
||||
</h1>
|
||||
|
||||
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
||||
<p>
|
||||
<%= if @user.confirmed_at do %>
|
||||
<%= gettext(
|
||||
"User was confirmed at%{confirmed_datetime}",
|
||||
confirmed_datetime: ""
|
||||
) %>
|
||||
<.datetime datetime={@user.confirmed_at} />
|
||||
<% else %>
|
||||
<%= gettext("Email unconfirmed") %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= gettext(
|
||||
"User registered on%{registered_datetime}",
|
||||
registered_datetime: ""
|
||||
) %>
|
||||
<.datetime datetime={@user.inserted_at} />
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
@ -3,51 +3,58 @@ defmodule CanneryWeb.ExportController do
|
||||
alias Cannery.{ActivityLog, Ammo, Containers}
|
||||
|
||||
def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do
|
||||
ammo_types =
|
||||
Ammo.list_ammo_types(current_user)
|
||||
|> Enum.map(fn ammo_type ->
|
||||
average_cost = ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user)
|
||||
round_count = ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
|
||||
used_count = ammo_type |> Ammo.get_used_count_for_ammo_type(current_user)
|
||||
ammo_group_count = ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true)
|
||||
ammo_types = Ammo.list_ammo_types(current_user, :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)
|
||||
|
||||
total_ammo_group_counts =
|
||||
ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true)
|
||||
|
||||
average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user)
|
||||
|
||||
ammo_types =
|
||||
ammo_types
|
||||
|> Enum.map(fn %{id: ammo_type_id} = ammo_type ->
|
||||
ammo_type
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|> Map.merge(%{
|
||||
"average_cost" => average_cost,
|
||||
"round_count" => round_count,
|
||||
"used_count" => used_count,
|
||||
"ammo_group_count" => ammo_group_count
|
||||
"average_cost" => Map.get(average_costs, ammo_type_id),
|
||||
"round_count" => Map.get(round_counts, ammo_type_id, 0),
|
||||
"used_count" => Map.get(used_counts, ammo_type_id, 0),
|
||||
"ammo_group_count" => Map.get(ammo_group_counts, ammo_type_id, 0),
|
||||
"total_ammo_group_count" => Map.get(total_ammo_group_counts, ammo_type_id, 0)
|
||||
})
|
||||
end)
|
||||
|
||||
ammo_groups =
|
||||
Ammo.list_ammo_groups(nil, true, current_user)
|
||||
|> Enum.map(fn ammo_group ->
|
||||
cpr = ammo_group |> Ammo.get_cpr()
|
||||
used_count = ammo_group |> Ammo.get_used_count()
|
||||
original_count = ammo_group |> Ammo.get_original_count()
|
||||
percentage_remaining = ammo_group |> Ammo.get_percentage_remaining()
|
||||
ammo_groups = 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)
|
||||
percentages_remaining = ammo_groups |> Ammo.get_percentages_remaining(current_user)
|
||||
|
||||
ammo_groups =
|
||||
ammo_groups
|
||||
|> Enum.map(fn %{id: ammo_group_id} = ammo_group ->
|
||||
ammo_group
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|> Map.merge(%{
|
||||
"used_count" => used_count,
|
||||
"percentage_remaining" => percentage_remaining,
|
||||
"original_count" => original_count,
|
||||
"cpr" => cpr
|
||||
"used_count" => Map.get(used_counts, ammo_group_id),
|
||||
"percentage_remaining" => Map.fetch!(percentages_remaining, ammo_group_id),
|
||||
"original_count" => Map.get(original_counts, ammo_group_id),
|
||||
"cpr" => Map.get(cprs, ammo_group_id)
|
||||
})
|
||||
end)
|
||||
|
||||
shot_groups = ActivityLog.list_shot_groups(current_user)
|
||||
shot_groups = ActivityLog.list_shot_groups(:all, current_user)
|
||||
|
||||
containers =
|
||||
Containers.list_containers(current_user)
|
||||
|> Enum.map(fn container ->
|
||||
ammo_group_count = container |> Containers.get_container_ammo_group_count!()
|
||||
round_count = container |> Containers.get_container_rounds!()
|
||||
ammo_group_count = container |> Ammo.get_ammo_groups_count_for_container!(current_user)
|
||||
round_count = container |> Ammo.get_round_count_for_container!(current_user)
|
||||
|
||||
container
|
||||
|> Jason.encode!()
|
||||
|
@ -3,6 +3,7 @@ defmodule CanneryWeb.UserRegistrationController do
|
||||
import CanneryWeb.Gettext
|
||||
alias Cannery.{Accounts, Accounts.Invites}
|
||||
alias CanneryWeb.{Endpoint, HomeLive}
|
||||
alias Ecto.Changeset
|
||||
|
||||
def new(conn, %{"invite" => invite_token}) do
|
||||
if Invites.valid_invite_token?(invite_token) do
|
||||
@ -70,7 +71,7 @@ defmodule CanneryWeb.UserRegistrationController do
|
||||
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|
||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
|
||||
end
|
||||
end
|
||||
|
@ -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 =
|
||||
@ -44,7 +44,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"ammo_group" => ammo_group_params}, socket) do
|
||||
{:noreply, socket |> assign_changeset(ammo_group_params)}
|
||||
{:noreply, socket |> assign_changeset(ammo_group_params, :validate)}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
@ -56,6 +56,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
end
|
||||
|
||||
# HTML Helpers
|
||||
|
||||
@spec container_options([Container.t()]) :: [{String.t(), Container.id()}]
|
||||
defp container_options(containers) do
|
||||
containers |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
|
||||
@ -70,35 +71,28 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
|
||||
defp assign_changeset(
|
||||
%{assigns: %{action: action, ammo_group: ammo_group, current_user: user}} = socket,
|
||||
ammo_group_params
|
||||
ammo_group_params,
|
||||
changeset_action \\ nil
|
||||
) do
|
||||
changeset_action =
|
||||
cond do
|
||||
action in [:new, :clone] -> :insert
|
||||
action == :edit -> :update
|
||||
default_action =
|
||||
case action do
|
||||
create when create in [:new, :clone] -> :insert
|
||||
:edit -> :update
|
||||
end
|
||||
|
||||
changeset =
|
||||
cond do
|
||||
action in [:new, :clone] ->
|
||||
ammo_type =
|
||||
if ammo_group_params |> Map.has_key?("ammo_type_id"),
|
||||
do: ammo_group_params |> Map.get("ammo_type_id") |> Ammo.get_ammo_type!(user),
|
||||
else: nil
|
||||
|
||||
container =
|
||||
if ammo_group_params |> Map.has_key?("container_id"),
|
||||
do: ammo_group_params |> Map.get("container_id") |> Containers.get_container!(user),
|
||||
else: nil
|
||||
|
||||
case default_action do
|
||||
:insert ->
|
||||
ammo_type = maybe_get_ammo_type(ammo_group_params, user)
|
||||
container = maybe_get_container(ammo_group_params, user)
|
||||
ammo_group |> AmmoGroup.create_changeset(ammo_type, container, user, ammo_group_params)
|
||||
|
||||
action == :edit ->
|
||||
:update ->
|
||||
ammo_group |> AmmoGroup.update_changeset(ammo_group_params, user)
|
||||
end
|
||||
|
||||
changeset =
|
||||
case changeset |> Changeset.apply_action(changeset_action) do
|
||||
case changeset |> Changeset.apply_action(changeset_action || default_action) do
|
||||
{:ok, _data} -> changeset
|
||||
{:error, changeset} -> changeset
|
||||
end
|
||||
@ -106,6 +100,20 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
socket |> assign(:changeset, changeset)
|
||||
end
|
||||
|
||||
defp maybe_get_container(%{"container_id" => container_id}, user)
|
||||
when is_binary(container_id) do
|
||||
container_id |> Containers.get_container!(user)
|
||||
end
|
||||
|
||||
defp maybe_get_container(_params_not_found, _user), do: nil
|
||||
|
||||
defp maybe_get_ammo_type(%{"ammo_type_id" => ammo_type_id}, user)
|
||||
when is_binary(ammo_type_id) do
|
||||
ammo_type_id |> Ammo.get_ammo_type!(user)
|
||||
end
|
||||
|
||||
defp maybe_get_ammo_type(_params_not_found, _user), do: nil
|
||||
|
||||
defp save_ammo_group(
|
||||
%{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} =
|
||||
socket,
|
||||
@ -146,16 +154,18 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
multiplier: multiplier
|
||||
)
|
||||
|
||||
{:error, changeset} =
|
||||
changeset
|
||||
|> Changeset.add_error(:multiplier, error_msg)
|
||||
|> Changeset.apply_action(:insert)
|
||||
|
||||
socket |> assign(:changeset, changeset)
|
||||
save_multiplier_error(socket, changeset, error_msg)
|
||||
|
||||
:error ->
|
||||
error_msg = dgettext("errors", "Could not parse number of copies")
|
||||
save_multiplier_error(socket, changeset, error_msg)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@spec save_multiplier_error(Socket.t(), Changeset.t(), String.t()) :: Socket.t()
|
||||
defp save_multiplier_error(socket, changeset, error_msg) do
|
||||
{:error, changeset} =
|
||||
changeset
|
||||
|> Changeset.add_error(:multiplier, error_msg)
|
||||
@ -164,9 +174,6 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
|
||||
socket |> assign(:changeset, changeset)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp create_multiple(
|
||||
%{assigns: %{current_user: current_user, return_to: return_to}} = socket,
|
||||
ammo_group_params,
|
||||
|
@ -49,8 +49,10 @@
|
||||
|
||||
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :notes,
|
||||
id: "ammo-group-form-notes",
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
phx_hook: "MaintainAttrs"
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :notes, "col-span-3 text-center") %>
|
||||
|
||||
|
@ -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
|
||||
@ -91,7 +91,6 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> display_ammo_groups()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"toggle_staged",
|
||||
%{"ammo_group_id" => id},
|
||||
@ -105,12 +104,10 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
|
||||
{:noreply, socket |> display_ammo_groups()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
|
||||
{:noreply, socket |> assign(:show_used, !show_used) |> display_ammo_groups()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :index))}
|
||||
end
|
||||
@ -122,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)
|
||||
|
||||
@ -133,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,20 +37,44 @@
|
||||
<.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">
|
||||
<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={:search}
|
||||
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"
|
||||
data-qa="ammo_group_search"
|
||||
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,
|
||||
placeholder: gettext("Search ammo")
|
||||
) %>
|
||||
@ -77,6 +98,7 @@
|
||||
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">
|
||||
@ -91,7 +113,9 @@
|
||||
phx-click="toggle_staged"
|
||||
phx-value-ammo_group_id={ammo_group.id}
|
||||
>
|
||||
<%= if ammo_group.staged, do: gettext("Unstage"), else: gettext("Stage") %>
|
||||
<%= if ammo_group.staged,
|
||||
do: dgettext("actions", "Unstage"),
|
||||
else: dgettext("actions", "Stage") %>
|
||||
</button>
|
||||
|
||||
<.link
|
||||
@ -102,7 +126,7 @@
|
||||
</.link>
|
||||
</div>
|
||||
</:range>
|
||||
<:container :let={%{container: %{name: container_name} = container} = ammo_group}>
|
||||
<: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)}
|
||||
@ -115,16 +139,20 @@
|
||||
patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)}
|
||||
class="mx-2 my-1 text-sm btn btn-primary"
|
||||
>
|
||||
<%= gettext("Move ammo") %>
|
||||
<%= dgettext("actions", "Move ammo") %>
|
||||
</.link>
|
||||
</div>
|
||||
</:container>
|
||||
<:actions :let={ammo_group}>
|
||||
<: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"
|
||||
data-qa={"view-#{ammo_group.id}"}
|
||||
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>
|
||||
@ -132,7 +160,11 @@
|
||||
<.link
|
||||
patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{ammo_group.id}"}
|
||||
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>
|
||||
@ -140,7 +172,11 @@
|
||||
<.link
|
||||
patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"clone-#{ammo_group.id}"}
|
||||
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>
|
||||
@ -151,7 +187,11 @@
|
||||
phx-click="delete"
|
||||
phx-value-id={ammo_group.id}
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
|
||||
data-qa={"delete-#{ammo_group.id}"}
|
||||
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>
|
||||
@ -159,10 +199,11 @@
|
||||
</:actions>
|
||||
</.live_component>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= cond do %>
|
||||
<% @live_action in [:new, :edit, :clone] -> %>
|
||||
<%= case @live_action do %>
|
||||
<% create when create in [:new, :edit, :clone] -> %>
|
||||
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.AmmoGroupLive.FormComponent}
|
||||
@ -174,7 +215,7 @@
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
<% @live_action == :add_shot_group -> %>
|
||||
<% :add_shot_group -> %>
|
||||
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.Components.AddShotGroupComponent}
|
||||
@ -186,7 +227,7 @@
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
<% @live_action == :move -> %>
|
||||
<% :move -> %>
|
||||
<.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.Components.MoveAmmoGroupComponent}
|
||||
@ -198,6 +239,5 @@
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
<% true -> %>
|
||||
<%= nil %>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
|
@ -4,8 +4,9 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.ContainerCard
|
||||
alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
|
||||
alias Cannery.{ActivityLog, ActivityLog.ShotGroup}
|
||||
alias Cannery.{Ammo, Ammo.AmmoGroup}
|
||||
alias Cannery.{ComparableDate, Containers}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@ -28,7 +29,6 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _url, %{assigns: %{live_action: live_action}} = socket) do
|
||||
socket =
|
||||
socket
|
||||
@ -58,7 +58,6 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"toggle_staged",
|
||||
_params,
|
||||
@ -70,7 +69,6 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
{:noreply, socket |> display_ammo_group(ammo_group)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"delete_shot_group",
|
||||
%{"id" => id},
|
||||
@ -85,30 +83,45 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
end
|
||||
|
||||
@spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t()
|
||||
defp display_ammo_group(socket, %AmmoGroup{} = ammo_group) do
|
||||
ammo_group = ammo_group |> Repo.preload([:container, :ammo_type, :shot_groups], force: true)
|
||||
|
||||
defp display_ammo_group(
|
||||
%{assigns: %{current_user: current_user}} = socket,
|
||||
%AmmoGroup{container_id: container_id} = ammo_group
|
||||
) do
|
||||
columns = [
|
||||
%{label: gettext("Rounds shot"), key: :count},
|
||||
%{label: gettext("Notes"), key: :notes},
|
||||
%{label: gettext("Date"), key: :date},
|
||||
%{label: nil, key: :actions, sortable: false}
|
||||
%{label: gettext("Date"), key: :date, type: ComparableDate},
|
||||
%{label: gettext("Actions"), key: :actions, sortable: false}
|
||||
]
|
||||
|
||||
shot_groups = ActivityLog.list_shot_groups_for_ammo_group(ammo_group, current_user)
|
||||
|
||||
rows =
|
||||
ammo_group.shot_groups
|
||||
shot_groups
|
||||
|> Enum.map(fn shot_group ->
|
||||
ammo_group |> get_table_row_for_shot_group(shot_group, columns)
|
||||
end)
|
||||
|
||||
socket |> assign(ammo_group: ammo_group, columns: columns, rows: rows)
|
||||
socket
|
||||
|> assign(
|
||||
ammo_group: ammo_group,
|
||||
original_count: Ammo.get_original_count(ammo_group, current_user),
|
||||
percentage_remaining: Ammo.get_percentage_remaining(ammo_group, current_user),
|
||||
container: container_id && Containers.get_container!(container_id, current_user),
|
||||
shot_groups: shot_groups,
|
||||
columns: columns,
|
||||
rows: rows
|
||||
)
|
||||
end
|
||||
|
||||
defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id),
|
||||
do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user))
|
||||
|
||||
@spec display_currency(float()) :: String.t()
|
||||
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
|
||||
|
||||
@spec get_table_row_for_shot_group(AmmoGroup.t(), ShotGroup.t(), [map()]) :: map()
|
||||
defp get_table_row_for_shot_group(ammo_group, %{date: date} = shot_group, columns) do
|
||||
defp get_table_row_for_shot_group(ammo_group, %{id: id, date: date} = shot_group, columns) do
|
||||
assigns = %{ammo_group: ammo_group, shot_group: shot_group}
|
||||
|
||||
columns
|
||||
@ -116,11 +129,11 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
value =
|
||||
case key do
|
||||
:date ->
|
||||
assigns = %{date: date}
|
||||
assigns = %{id: id, date: date}
|
||||
|
||||
{date,
|
||||
~H"""
|
||||
<.date date={@date} />
|
||||
<.date id={"#{@id}-date"} date={@date} />
|
||||
"""}
|
||||
|
||||
:actions ->
|
||||
@ -129,7 +142,11 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
<.link
|
||||
patch={Routes.ammo_group_show_path(Endpoint, :edit_shot_group, @ammo_group, @shot_group)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{@shot_group.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit shot group of %{shot_group_count} shots",
|
||||
shot_group_count: @shot_group.count
|
||||
)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -140,7 +157,11 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
|
||||
phx-click="delete_shot_group"
|
||||
phx-value-id={@shot_group.id}
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to delete this shot record?")}
|
||||
data-qa={"delete-#{@shot_group.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete shot record of %{shot_group_count} shots",
|
||||
shot_group_count: @shot_group.count
|
||||
)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
|
@ -11,12 +11,12 @@
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Original count:") %>
|
||||
<%= Ammo.get_original_count(@ammo_group) %>
|
||||
<%= @original_count %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Percentage left:") %>
|
||||
<%= gettext("%{percentage}%", percentage: @ammo_group |> Ammo.get_percentage_remaining()) %>
|
||||
<%= gettext("%{percentage}%", percentage: @percentage_remaining) %>
|
||||
</span>
|
||||
|
||||
<%= if @ammo_group.notes do %>
|
||||
@ -28,23 +28,19 @@
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Purchased on:") %>
|
||||
<.date date={@ammo_group.purchased_on} />
|
||||
<.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} />
|
||||
</span>
|
||||
|
||||
<%= if @ammo_group.price_paid do %>
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Original cost:") %>
|
||||
<%= gettext("$%{amount}",
|
||||
amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
|
||||
) %>
|
||||
<%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= gettext("Current value:") %>
|
||||
<%= gettext("$%{amount}",
|
||||
amount:
|
||||
(@ammo_group.price_paid * Ammo.get_percentage_remaining(@ammo_group) / 100)
|
||||
|> :erlang.float_to_binary(decimals: 2)
|
||||
amount: display_currency(@ammo_group.price_paid * @percentage_remaining / 100)
|
||||
) %>
|
||||
</span>
|
||||
<% end %>
|
||||
@ -55,7 +51,6 @@
|
||||
<.link
|
||||
navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_group.ammo_type)}
|
||||
class="mx-4 my-2 btn btn-primary"
|
||||
data-qa="details"
|
||||
>
|
||||
<%= dgettext("actions", "View in Catalog") %>
|
||||
</.link>
|
||||
@ -63,7 +58,11 @@
|
||||
<.link
|
||||
patch={Routes.ammo_group_show_path(Endpoint, :edit, @ammo_group)}
|
||||
class="mx-4 my-2 text-primary-600 link"
|
||||
data-qa="edit"
|
||||
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>
|
||||
@ -73,7 +72,11 @@
|
||||
class="mx-4 my-2 text-primary-600 link"
|
||||
phx-click="delete"
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
|
||||
data-qa="delete"
|
||||
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>
|
||||
@ -89,9 +92,8 @@
|
||||
<.link
|
||||
patch={Routes.ammo_group_show_path(Endpoint, :move, @ammo_group)}
|
||||
class="btn btn-primary"
|
||||
data-qa="move"
|
||||
>
|
||||
<%= dgettext("actions", "Move containers") %>
|
||||
<%= dgettext("actions", "Move ammo") %>
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
@ -106,18 +108,18 @@
|
||||
<hr class="mb-4 w-full" />
|
||||
|
||||
<div>
|
||||
<%= if @ammo_group.container do %>
|
||||
<%= if @container do %>
|
||||
<h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl">
|
||||
<%= gettext("Stored in") %>
|
||||
</h1>
|
||||
|
||||
<.container_card container={@ammo_group.container} />
|
||||
<.container_card container={@container} current_user={@current_user} />
|
||||
<% else %>
|
||||
<%= gettext("This ammo is not in a container") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= unless @ammo_group.shot_groups |> Enum.empty?() do %>
|
||||
<%= unless @shot_groups |> Enum.empty?() do %>
|
||||
<hr class="mb-4 w-full" />
|
||||
|
||||
<h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl">
|
||||
|
@ -35,15 +35,18 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
|
||||
ammo_type_params
|
||||
) do
|
||||
changeset_action =
|
||||
cond do
|
||||
action in [:new, :clone] -> :insert
|
||||
action == :edit -> :update
|
||||
case action do
|
||||
create when create in [:new, :clone] -> :insert
|
||||
:edit -> :update
|
||||
end
|
||||
|
||||
changeset =
|
||||
cond do
|
||||
action in [:new, :clone] -> ammo_type |> AmmoType.create_changeset(user, ammo_type_params)
|
||||
action == :edit -> ammo_type |> AmmoType.update_changeset(ammo_type_params)
|
||||
case action do
|
||||
create when create in [:new, :clone] ->
|
||||
ammo_type |> AmmoType.create_changeset(user, ammo_type_params)
|
||||
|
||||
:edit ->
|
||||
ammo_type |> AmmoType.update_changeset(ammo_type_params)
|
||||
end
|
||||
|
||||
changeset =
|
||||
|
@ -15,91 +15,99 @@
|
||||
: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") %>
|
||||
<%= text_input(f, :name,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :name, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :desc,
|
||||
id: "ammo-type-form-desc",
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
phx_hook: "MaintainAttrs"
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :desc, "col-span-3 text-center") %>
|
||||
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/Bullet#Abbreviations"
|
||||
class="col-span-3 text-center link title text-md text-primary-600"
|
||||
>
|
||||
<%= gettext("Example bullet type abbreviations") %>
|
||||
</a>
|
||||
<%= 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",
|
||||
placeholder: gettext("FMJ")
|
||||
) %>
|
||||
<%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
|
||||
|
||||
<%= 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",
|
||||
placeholder: gettext("Steel")
|
||||
) %>
|
||||
<%= error_tag(f, :bullet_core, "col-span-3 text-center") %>
|
||||
<h2 class="text-center title text-lg text-primary-600 col-span-3">
|
||||
<%= gettext("Dimensions") %>
|
||||
</h2>
|
||||
|
||||
<%= 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",
|
||||
placeholder: "5.56x46mm NATO"
|
||||
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, :caliber, gettext("Caliber"), class: "title text-lg text-primary-600") %>
|
||||
<%= label(
|
||||
f,
|
||||
:caliber,
|
||||
if(Changeset.get_field(@changeset, :type) == :shotgun,
|
||||
do: gettext("Gauge"),
|
||||
else: gettext("Caliber")
|
||||
),
|
||||
class: "title text-lg text-primary-600"
|
||||
) %>
|
||||
<%= text_input(f, :caliber,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
placeholder: ".223"
|
||||
maxlength: 255,
|
||||
placeholder: gettext(".223")
|
||||
) %>
|
||||
<%= 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",
|
||||
placeholder: gettext("Brass")
|
||||
) %>
|
||||
<%= error_tag(f, :case_material, "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",
|
||||
placeholder: gettext("Bimetal")
|
||||
) %>
|
||||
<%= error_tag(f, :case_material, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :muzzle_velocity, gettext("Muzzle velocity"),
|
||||
<%= if Changeset.get_field(@changeset, :type) == :shotgun do %>
|
||||
<%= label(f, :unfired_length, gettext("Unfired shell length"),
|
||||
class: "title text-lg text-primary-600"
|
||||
) %>
|
||||
<%= number_input(f, :muzzle_velocity,
|
||||
step: "1",
|
||||
<%= text_input(f, :unfired_length,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
min: 1
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %>
|
||||
<%= error_tag(f, :unfired_length, "col-span-3 text-center") %>
|
||||
|
||||
<%= 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") %>
|
||||
<%= 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",
|
||||
<%= 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",
|
||||
min: 1
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %>
|
||||
<%= error_tag(f, :brass_height, "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 %>
|
||||
|
||||
<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,
|
||||
@ -109,27 +117,202 @@
|
||||
) %>
|
||||
<%= 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",
|
||||
placeholder: "+P"
|
||||
maxlength: 255,
|
||||
placeholder: gettext("+P")
|
||||
) %>
|
||||
<%= 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",
|
||||
placeholder: "Boxer"
|
||||
maxlength: 255,
|
||||
placeholder: gettext("Boxer")
|
||||
) %>
|
||||
<%= error_tag(f, :primer_type, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :firing_type, gettext("Firing type"), class: "title text-lg text-primary-600") %>
|
||||
<%= text_input(f, :firing_type,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
placeholder: "Centerfire"
|
||||
maxlength: 255,
|
||||
placeholder: gettext("Centerfire")
|
||||
) %>
|
||||
<%= 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") %>
|
||||
@ -146,12 +329,22 @@
|
||||
<%= 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") %>
|
||||
<%= text_input(f, :manufacturer,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :manufacturer, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :upc, gettext("UPC"), class: "title text-lg text-primary-600") %>
|
||||
<%= text_input(f, :upc, class: "text-center col-span-2 input input-primary") %>
|
||||
<%= text_input(f, :upc,
|
||||
class: "text-center col-span-2 input input-primary",
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :upc, "col-span-3 text-center") %>
|
||||
|
||||
<%= submit(dgettext("actions", "Save"),
|
||||
|
@ -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
|
||||
@ -69,28 +69,46 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
|
||||
%{name: name} = Ammo.get_ammo_type!(id, current_user) |> Ammo.delete_ammo_type!(current_user)
|
||||
|
||||
prompt = dgettext("prompts", "%{name} deleted succesfully", name: name)
|
||||
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> list_ammo_types()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
|
||||
{:noreply, socket |> assign(:show_used, !show_used) |> list_ammo_types()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :index))}
|
||||
end
|
||||
|
||||
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
|
||||
{:noreply,
|
||||
socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :search, search_term))}
|
||||
search_path = Routes.ammo_type_index_path(Endpoint, :search, search_term)
|
||||
{:noreply, socket |> push_patch(to: search_path)}
|
||||
end
|
||||
|
||||
defp list_ammo_types(%{assigns: %{search: search, current_user: current_user}} = socket) do
|
||||
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,18 +17,43 @@
|
||||
<%= 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={:search}
|
||||
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"
|
||||
data-qa="ammo_type_search"
|
||||
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,
|
||||
placeholder: gettext("Search catalog")
|
||||
) %>
|
||||
@ -54,13 +79,16 @@
|
||||
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">
|
||||
<.link
|
||||
navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"view-#{ammo_type.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "View %{ammo_type_name}", ammo_type_name: ammo_type.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-eye"></i>
|
||||
</.link>
|
||||
@ -68,7 +96,9 @@
|
||||
<.link
|
||||
patch={Routes.ammo_type_index_path(Endpoint, :edit, ammo_type)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{ammo_type.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit %{ammo_type_name}", ammo_type_name: ammo_type.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -76,7 +106,9 @@
|
||||
<.link
|
||||
patch={Routes.ammo_type_index_path(Endpoint, :clone, ammo_type)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"clone-#{ammo_type.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Clone %{ammo_type_name}", ammo_type_name: ammo_type.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-copy"></i>
|
||||
</.link>
|
||||
@ -93,7 +125,9 @@
|
||||
name: ammo_type.name
|
||||
)
|
||||
}
|
||||
data-qa={"delete-#{ammo_type.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete %{ammo_type_name}", ammo_type_name: ammo_type.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
|
@ -4,44 +4,16 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.AmmoGroupCard
|
||||
alias Cannery.{Ammo, Ammo.AmmoType}
|
||||
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)}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, %{assigns: %{live_action: live_action}} = socket),
|
||||
do: {:ok, socket |> assign(show_used: false, view_table: live_action == :table)}
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _params, %{assigns: %{live_action: live_action}} = socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(view_table: live_action == :table)
|
||||
|> display_ammo_type(id)
|
||||
|
||||
{:noreply, socket}
|
||||
def handle_params(%{"id" => id}, _params, socket) do
|
||||
{:noreply, socket |> display_ammo_type(id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -58,32 +30,21 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
|
||||
{:noreply, socket |> assign(:show_used, !show_used) |> display_ammo_type()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"toggle_table",
|
||||
_params,
|
||||
%{assigns: %{view_table: view_table, ammo_type: ammo_type}} = socket
|
||||
) do
|
||||
new_path =
|
||||
if view_table,
|
||||
do: Routes.ammo_type_show_path(Endpoint, :show, ammo_type),
|
||||
else: Routes.ammo_type_show_path(Endpoint, :table, ammo_type)
|
||||
|
||||
{:noreply, socket |> push_patch(to: new_path)}
|
||||
def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
|
||||
{:noreply, socket |> assign(:view_table, !view_table)}
|
||||
end
|
||||
|
||||
defp display_ammo_type(
|
||||
%{assigns: %{live_action: live_action, current_user: current_user, show_used: show_used}} =
|
||||
socket,
|
||||
%AmmoType{} = ammo_type
|
||||
%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
|
||||
@ -94,14 +55,56 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
|
||||
ammo_type |> Map.get(field) != default_value
|
||||
end)
|
||||
|
||||
ammo_groups = ammo_type |> Ammo.list_ammo_groups_for_type(current_user, show_used)
|
||||
|
||||
[
|
||||
original_counts,
|
||||
used_packs_count,
|
||||
historical_packs_count,
|
||||
used_rounds,
|
||||
historical_round_count
|
||||
] =
|
||||
if show_used do
|
||||
[
|
||||
ammo_groups |> Ammo.get_original_counts(current_user),
|
||||
ammo_type |> Ammo.get_used_ammo_groups_count_for_type(current_user),
|
||||
ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true),
|
||||
ammo_type |> ActivityLog.get_used_count_for_ammo_type(current_user),
|
||||
ammo_type |> Ammo.get_historical_count_for_ammo_type(current_user)
|
||||
]
|
||||
else
|
||||
[nil, nil, nil, nil, nil]
|
||||
end
|
||||
|
||||
page_title =
|
||||
case live_action do
|
||||
:show -> ammo_type_name
|
||||
:edit -> gettext("Edit %{ammo_type_name}", ammo_type_name: ammo_type_name)
|
||||
end
|
||||
|
||||
containers =
|
||||
ammo_groups
|
||||
|> Enum.map(fn %{container_id: container_id} -> container_id end)
|
||||
|> Containers.get_containers(current_user)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
page_title: page_title(live_action, ammo_type),
|
||||
page_title: page_title,
|
||||
ammo_type: ammo_type,
|
||||
ammo_groups: ammo_type |> Ammo.list_ammo_groups_for_type(current_user, show_used),
|
||||
avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user),
|
||||
fields_list: @fields_list,
|
||||
fields_to_display: fields_to_display
|
||||
ammo_groups: ammo_groups,
|
||||
containers: containers,
|
||||
cprs: ammo_groups |> Ammo.get_cprs(current_user),
|
||||
last_used_dates: ammo_groups |> ActivityLog.get_last_used_dates(current_user),
|
||||
avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type(current_user),
|
||||
rounds: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user),
|
||||
original_counts: original_counts,
|
||||
used_rounds: used_rounds,
|
||||
historical_round_count: historical_round_count,
|
||||
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_to_display: fields_to_display(ammo_type),
|
||||
custom_fields?: custom_fields?
|
||||
)
|
||||
end
|
||||
|
||||
@ -113,9 +116,48 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
|
||||
socket |> display_ammo_type(ammo_type)
|
||||
end
|
||||
|
||||
defp page_title(action, %{name: ammo_type_name}) when action in [:show, :table],
|
||||
do: ammo_type_name
|
||||
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
|
||||
|
||||
defp page_title(:edit, %{name: ammo_type_name}),
|
||||
do: gettext("Edit %{ammo_type_name}", ammo_type_name: ammo_type_name)
|
||||
@spec display_currency(float()) :: String.t()
|
||||
defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2)
|
||||
end
|
||||
|
@ -16,7 +16,7 @@
|
||||
<.link
|
||||
patch={Routes.ammo_type_show_path(Endpoint, :edit, @ammo_type)}
|
||||
class="text-primary-600 link"
|
||||
data-qa="edit"
|
||||
aria-label={dgettext("actions", "Edit %{ammo_type_name}", ammo_type_name: @ammo_type.name)}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -32,7 +32,9 @@
|
||||
name: @ammo_type.name
|
||||
)
|
||||
}
|
||||
data-qa="delete"
|
||||
aria-label={
|
||||
dgettext("actions", "Delete %{ammo_type_name}", ammo_type_name: @ammo_type.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
@ -40,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 %>
|
||||
@ -69,15 +88,16 @@
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_round_count_for_ammo_type(@current_user) %>
|
||||
<%= @rounds %>
|
||||
</span>
|
||||
|
||||
<%= if @show_used do %>
|
||||
<h3 class="title text-lg">
|
||||
<%= gettext("Used rounds:") %>
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_used_count_for_ammo_type(@current_user) %>
|
||||
<%= @used_rounds %>
|
||||
</span>
|
||||
|
||||
<h3 class="title text-lg">
|
||||
@ -85,27 +105,25 @@
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_historical_count_for_ammo_type(@current_user) %>
|
||||
<%= @historical_round_count %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
|
||||
<h3 class="title text-lg">
|
||||
<%= gettext("Packs:") %>
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_ammo_groups_count_for_type(@current_user) %>
|
||||
<%= @packs_count %>
|
||||
</span>
|
||||
|
||||
<%= if @show_used do %>
|
||||
<h3 class="title text-lg">
|
||||
<%= gettext("Used packs:") %>
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_used_ammo_groups_count_for_type(@current_user) %>
|
||||
<%= @used_packs_count %>
|
||||
</span>
|
||||
|
||||
<h3 class="title text-lg">
|
||||
@ -113,19 +131,16 @@
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= @ammo_type |> Ammo.get_ammo_groups_count_for_type(@current_user, true) %>
|
||||
<%= @historical_packs_count %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
|
||||
<h3 class="title text-lg">
|
||||
<%= gettext("Added on:") %>
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<.datetime datetime={@ammo_type.inserted_at} />
|
||||
<.datetime id={"#{@ammo_type.id}-inserted-at"} datetime={@ammo_type.inserted_at} />
|
||||
</span>
|
||||
|
||||
<%= if @avg_cost_per_round do %>
|
||||
@ -134,9 +149,7 @@
|
||||
</h3>
|
||||
|
||||
<span class="text-primary-600">
|
||||
<%= gettext("$%{amount}",
|
||||
amount: @avg_cost_per_round |> :erlang.float_to_binary(decimals: 2)
|
||||
) %>
|
||||
<%= gettext("$%{amount}", amount: display_currency(@avg_cost_per_round)) %>
|
||||
</span>
|
||||
<% else %>
|
||||
<h3 class="mx-8 my-4 title text-lg text-primary-600 col-span-2">
|
||||
@ -174,8 +187,9 @@
|
||||
id="ammo-type-show-table"
|
||||
ammo_groups={@ammo_groups}
|
||||
current_user={@current_user}
|
||||
show_used={@show_used}
|
||||
>
|
||||
<:container :let={%{container: %{name: container_name} = container}}>
|
||||
<:container :let={{_ammo_group, %{name: container_name} = container}}>
|
||||
<.link
|
||||
navigate={Routes.container_show_path(Endpoint, :show, container)}
|
||||
class="mx-2 my-1 link"
|
||||
@ -183,13 +197,32 @@
|
||||
<%= container_name %>
|
||||
</.link>
|
||||
</: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>
|
||||
</div>
|
||||
</:actions>
|
||||
</.live_component>
|
||||
<% else %>
|
||||
<div class="flex flex-wrap justify-center items-stretch">
|
||||
<.ammo_group_card
|
||||
:for={ammo_group <- @ammo_groups}
|
||||
:for={%{id: ammo_group_id, container_id: container_id} = ammo_group <- @ammo_groups}
|
||||
ammo_group={ammo_group}
|
||||
show_container={true}
|
||||
original_count={@original_counts && Map.fetch!(@original_counts, ammo_group_id)}
|
||||
cpr={Map.get(@cprs, ammo_group_id)}
|
||||
last_used_date={Map.get(@last_used_dates, ammo_group_id)}
|
||||
current_user={@current_user}
|
||||
container={Map.fetch!(@containers, container_id)}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -4,25 +4,41 @@ defmodule CanneryWeb.ContainerLive.EditTagsComponent do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, Containers, Containers.Container, Repo, Tags, Tags.Tag}
|
||||
alias Cannery.{Accounts.User, Containers}
|
||||
alias Cannery.Containers.{Container, Tag}
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@impl true
|
||||
@spec update(
|
||||
%{:container => Container.t(), :current_user => User.t(), optional(any) => any},
|
||||
%{
|
||||
:container => Container.t(),
|
||||
:current_path => String.t(),
|
||||
:current_user => User.t(),
|
||||
optional(any) => any
|
||||
},
|
||||
Socket.t()
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{container: container, current_user: current_user} = assigns, socket) do
|
||||
tags = Tags.list_tags(current_user)
|
||||
container = container |> Repo.preload(:tags)
|
||||
{:ok, socket |> assign(assigns) |> assign(tags: tags, container: container)}
|
||||
def update(
|
||||
%{container: _container, current_path: _current_path, current_user: current_user} =
|
||||
assigns,
|
||||
socket
|
||||
) do
|
||||
tags = Containers.list_tags(current_user)
|
||||
{:ok, socket |> assign(assigns) |> assign(:tags, tags)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"save",
|
||||
%{"tag" => %{"tag_id" => tag_id}},
|
||||
%{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
|
||||
%{
|
||||
assigns: %{
|
||||
tags: tags,
|
||||
container: container,
|
||||
current_user: current_user,
|
||||
current_path: current_path
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
socket =
|
||||
case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
|
||||
@ -32,19 +48,24 @@ defmodule CanneryWeb.ContainerLive.EditTagsComponent do
|
||||
|
||||
%{name: tag_name} = tag ->
|
||||
_container_tag = Containers.add_tag!(container, tag, current_user)
|
||||
container = container |> Repo.preload(:tags, force: true)
|
||||
prompt = dgettext("prompts", "%{name} added successfully", name: tag_name)
|
||||
socket |> put_flash(:info, prompt) |> assign(container: container)
|
||||
socket |> put_flash(:info, prompt) |> push_patch(to: current_path)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"delete",
|
||||
%{"tag-id" => tag_id},
|
||||
%{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
|
||||
%{
|
||||
assigns: %{
|
||||
tags: tags,
|
||||
container: container,
|
||||
current_user: current_user,
|
||||
current_path: current_path
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
socket =
|
||||
case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
|
||||
@ -54,9 +75,8 @@ defmodule CanneryWeb.ContainerLive.EditTagsComponent do
|
||||
|
||||
%{name: tag_name} = tag ->
|
||||
_container_tag = Containers.remove_tag!(container, tag, current_user)
|
||||
container = container |> Repo.preload(:tags, force: true)
|
||||
prompt = dgettext("prompts", "%{name} removed successfully", name: tag_name)
|
||||
socket |> put_flash(:info, prompt) |> assign(container: container)
|
||||
socket |> put_flash(:info, prompt) |> push_patch(to: current_path)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
@ -36,7 +36,8 @@
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={:tag}
|
||||
for={%{}}
|
||||
as={:tag}
|
||||
id="add-tag-to-container-form"
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
phx-target={@myself}
|
||||
|
@ -35,17 +35,17 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
|
||||
container_params
|
||||
) do
|
||||
changeset_action =
|
||||
cond do
|
||||
action in [:new, :clone] -> :insert
|
||||
action == :edit -> :update
|
||||
case action do
|
||||
create when create in [:new, :clone] -> :insert
|
||||
:edit -> :update
|
||||
end
|
||||
|
||||
changeset =
|
||||
cond do
|
||||
action in [:new, :clone] ->
|
||||
case action do
|
||||
create when create in [:new, :clone] ->
|
||||
container |> Container.create_changeset(user, container_params)
|
||||
|
||||
action == :edit ->
|
||||
:edit ->
|
||||
container |> Container.update_changeset(container_params)
|
||||
end
|
||||
|
||||
|
@ -21,30 +21,36 @@
|
||||
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
|
||||
<%= text_input(f, :name,
|
||||
class: "input input-primary col-span-2",
|
||||
placeholder: gettext("My cool ammo can")
|
||||
placeholder: gettext("My cool ammo can"),
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :name, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :desc,
|
||||
id: "container-form-desc",
|
||||
class: "input input-primary col-span-2",
|
||||
placeholder: gettext("Metal ammo can with the anime girl sticker"),
|
||||
phx_hook: "MaintainAttrs",
|
||||
placeholder: gettext("Metal ammo can with the anime girl sticker")
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :desc, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :type, gettext("Type"), class: "title text-lg text-primary-600") %>
|
||||
<%= text_input(f, :type,
|
||||
class: "input input-primary col-span-2",
|
||||
placeholder: gettext("Magazine, Clip, Ammo Box, etc")
|
||||
placeholder: gettext("Magazine, Clip, Ammo Box, etc"),
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= error_tag(f, :type, "col-span-3 text-center") %>
|
||||
|
||||
<%= label(f, :location, gettext("Location"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :location,
|
||||
id: "container-form-location",
|
||||
class: "input input-primary col-span-2",
|
||||
placeholder: gettext("On the bookshelf"),
|
||||
phx_hook: "MaintainAttrs",
|
||||
placeholder: gettext("On the bookshelf")
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :location, "col-span-3 text-center") %>
|
||||
|
||||
|
@ -4,8 +4,7 @@ defmodule CanneryWeb.ContainerLive.Index do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.ContainerCard
|
||||
alias Cannery.{Containers, Containers.Container, Repo}
|
||||
alias Cannery.{Containers, Containers.Container}
|
||||
alias Ecto.Changeset
|
||||
|
||||
@impl true
|
||||
@ -23,10 +22,7 @@ defmodule CanneryWeb.ContainerLive.Index do
|
||||
end
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
|
||||
%{name: container_name} =
|
||||
container =
|
||||
Containers.get_container!(id, current_user)
|
||||
|> Repo.preload([:tags, :ammo_groups])
|
||||
%{name: container_name} = container = Containers.get_container!(id, current_user)
|
||||
|
||||
socket
|
||||
|> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
|
||||
@ -62,9 +58,7 @@ defmodule CanneryWeb.ContainerLive.Index do
|
||||
end
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
|
||||
%{name: container_name} =
|
||||
container =
|
||||
Containers.get_container!(id, current_user) |> Repo.preload([:tags, :ammo_groups])
|
||||
%{name: container_name} = container = Containers.get_container!(id, current_user)
|
||||
|
||||
page_title = gettext("Edit %{name} tags", name: container_name)
|
||||
socket |> assign(page_title: page_title, container: container)
|
||||
@ -106,12 +100,10 @@ defmodule CanneryWeb.ContainerLive.Index do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
|
||||
{:noreply, socket |> assign(:view_table, !view_table) |> display_containers()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.container_index_path(Endpoint, :index))}
|
||||
end
|
||||
@ -122,10 +114,6 @@ defmodule CanneryWeb.ContainerLive.Index do
|
||||
end
|
||||
|
||||
defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do
|
||||
containers =
|
||||
Containers.list_containers(search, current_user)
|
||||
|> Repo.preload([:tags, :ammo_groups])
|
||||
|
||||
socket |> assign(:containers, containers)
|
||||
socket |> assign(:containers, Containers.list_containers(search, current_user))
|
||||
end
|
||||
end
|
||||
|
@ -17,18 +17,19 @@
|
||||
<%= 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={:search}
|
||||
for={%{}}
|
||||
as={:search}
|
||||
phx-change="search"
|
||||
phx-submit="search"
|
||||
class="grow self-stretch flex flex-col items-stretch"
|
||||
data-qa="container_search"
|
||||
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,
|
||||
placeholder: gettext("Search containers")
|
||||
) %>
|
||||
@ -40,7 +41,6 @@
|
||||
</span>
|
||||
</.toggle_button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @containers |> Enum.empty?() do %>
|
||||
<h2 class="title text-xl text-primary-600">
|
||||
@ -61,6 +61,9 @@
|
||||
<.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>
|
||||
@ -70,7 +73,9 @@
|
||||
<.link
|
||||
patch={Routes.container_index_path(Endpoint, :edit, container)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{container.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit %{container_name}", container_name: container.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -78,7 +83,9 @@
|
||||
<.link
|
||||
patch={Routes.container_index_path(Endpoint, :clone, container)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"clone-#{container.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Clone %{container_name}", container_name: container.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-copy"></i>
|
||||
</.link>
|
||||
@ -89,9 +96,13 @@
|
||||
phx-click="delete"
|
||||
phx-value-id={container.id}
|
||||
data-confirm={
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name)
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?",
|
||||
name: container.name
|
||||
)
|
||||
}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete %{container_name}", container_name: container.name)
|
||||
}
|
||||
data-qa={"delete-#{container.id}"}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
@ -99,12 +110,19 @@
|
||||
</.live_component>
|
||||
<% else %>
|
||||
<div class="w-full flex flex-row flex-wrap justify-center items-stretch">
|
||||
<.container_card :for={container <- @containers} container={container}>
|
||||
<.container_card
|
||||
:for={container <- @containers}
|
||||
container={container}
|
||||
current_user={@current_user}
|
||||
>
|
||||
<: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>
|
||||
@ -113,7 +131,9 @@
|
||||
<.link
|
||||
patch={Routes.container_index_path(Endpoint, :edit, container)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{container.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit %{container_name}", container_name: container.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -121,7 +141,9 @@
|
||||
<.link
|
||||
patch={Routes.container_index_path(Endpoint, :clone, container)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"clone-#{container.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Clone %{container_name}", container_name: container.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-copy"></i>
|
||||
</.link>
|
||||
@ -132,9 +154,13 @@
|
||||
phx-click="delete"
|
||||
phx-value-id={container.id}
|
||||
data-confirm={
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name)
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?",
|
||||
name: container.name
|
||||
)
|
||||
}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete %{container_name}", container_name: container.name)
|
||||
}
|
||||
data-qa={"delete-#{container.id}"}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
@ -142,12 +168,12 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:new, :edit, :clone]}
|
||||
return_to={Routes.container_index_path(Endpoint, :index)}
|
||||
>
|
||||
<%= case @live_action do %>
|
||||
<% modifying when modifying in [:new, :edit, :clone] -> %>
|
||||
<.modal return_to={Routes.container_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.ContainerLive.FormComponent}
|
||||
id={@container.id || :new}
|
||||
@ -157,15 +183,18 @@
|
||||
return_to={Routes.container_index_path(Endpoint, :index)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal :if={@live_action == :edit_tags} return_to={Routes.container_index_path(Endpoint, :index)}>
|
||||
</.modal>
|
||||
<% :edit_tags -> %>
|
||||
<.modal return_to={Routes.container_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.ContainerLive.EditTagsComponent}
|
||||
id={@container.id}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
container={@container}
|
||||
current_path={Routes.container_index_path(Endpoint, :edit_tags, @container)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
</.modal>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
|
@ -4,15 +4,14 @@ defmodule CanneryWeb.ContainerLive.Show do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.{AmmoGroupCard, TagCard}
|
||||
alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container, Repo, Tags}
|
||||
alias Cannery.{Accounts.User, ActivityLog, Ammo, Containers, Containers.Container}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@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
|
||||
@ -31,7 +30,7 @@ defmodule CanneryWeb.ContainerLive.Show do
|
||||
%{assigns: %{container: container, current_user: current_user}} = socket
|
||||
) do
|
||||
socket =
|
||||
case Tags.get_tag(tag_id, current_user) do
|
||||
case Containers.get_tag(tag_id, current_user) do
|
||||
{:ok, tag} ->
|
||||
_count = Containers.remove_tag!(container, tag, current_user)
|
||||
|
||||
@ -43,14 +42,13 @@ defmodule CanneryWeb.ContainerLive.Show do
|
||||
|
||||
socket |> put_flash(:info, prompt) |> render_container()
|
||||
|
||||
{:error, error_string} ->
|
||||
socket |> put_flash(:error, error_string)
|
||||
{:error, :not_found} ->
|
||||
socket |> put_flash(:error, dgettext("errors", "Tag not found"))
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"delete_container",
|
||||
_params,
|
||||
@ -84,37 +82,56 @@ defmodule CanneryWeb.ContainerLive.Show do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
|
||||
{:noreply, socket |> assign(:show_used, !show_used) |> render_container()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
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)
|
||||
|> Repo.preload([:tags], force: true)
|
||||
|
||||
ammo_groups = Ammo.list_ammo_groups_for_container(container, current_user, show_used)
|
||||
%{name: container_name} = container = Containers.get_container!(id, current_user)
|
||||
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)
|
||||
|
||||
page_title =
|
||||
case live_action do
|
||||
action when action in [:show, :table] -> container_name
|
||||
:show -> container_name
|
||||
:edit -> gettext("Edit %{name}", name: container_name)
|
||||
:edit_tags -> gettext("Edit %{name} tags", name: container_name)
|
||||
end
|
||||
|
||||
socket |> assign(container: container, ammo_groups: ammo_groups, page_title: page_title)
|
||||
socket
|
||||
|> assign(
|
||||
container: container,
|
||||
round_count: Ammo.get_round_count_for_container!(container, current_user),
|
||||
ammo_groups_count: Ammo.get_ammo_groups_count_for_container!(container, current_user),
|
||||
ammo_groups: ammo_groups,
|
||||
original_counts: original_counts,
|
||||
cprs: cprs,
|
||||
last_used_dates: last_used_dates,
|
||||
page_title: page_title
|
||||
)
|
||||
end
|
||||
|
||||
@spec render_container(Socket.t()) :: Socket.t()
|
||||
|
@ -18,31 +18,21 @@
|
||||
<%= @container.location %>
|
||||
</span>
|
||||
|
||||
<%= unless @ammo_groups |> Enum.empty?() do %>
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= if @show_used do %>
|
||||
<%= gettext("Total packs:") %>
|
||||
<% else %>
|
||||
<%= gettext("Packs:") %>
|
||||
<% end %>
|
||||
<%= Enum.count(@ammo_groups) %>
|
||||
<%= @ammo_groups_count %>
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg title text-lg">
|
||||
<%= if @show_used do %>
|
||||
<%= gettext("Total rounds:") %>
|
||||
<% else %>
|
||||
<%= gettext("Rounds:") %>
|
||||
<% end %>
|
||||
<%= @container |> Containers.get_container_rounds!() %>
|
||||
<%= @round_count %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<div class="flex space-x-4 justify-center items-center text-primary-600">
|
||||
<.link
|
||||
patch={Routes.container_show_path(Endpoint, :edit, @container)}
|
||||
class="text-primary-600 link"
|
||||
data-qa="edit"
|
||||
aria-label={dgettext("actions", "Edit %{container_name}", container_name: @container.name)}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -54,7 +44,9 @@
|
||||
data-confirm={
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?", name: @container.name)
|
||||
}
|
||||
data-qa="delete"
|
||||
aria-label={
|
||||
dgettext("actions", "Delete %{container_name}", container_name: @container.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
@ -94,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">
|
||||
@ -119,6 +129,7 @@
|
||||
id="ammo-type-show-table"
|
||||
ammo_groups={@ammo_groups}
|
||||
current_user={@current_user}
|
||||
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">
|
||||
@ -128,17 +139,23 @@
|
||||
</.live_component>
|
||||
<% else %>
|
||||
<div class="flex flex-wrap justify-center items-stretch">
|
||||
<.ammo_group_card :for={ammo_group <- @ammo_groups} ammo_group={ammo_group} />
|
||||
<.ammo_group_card
|
||||
:for={%{id: ammo_group_id} = ammo_group <- @ammo_groups}
|
||||
ammo_group={ammo_group}
|
||||
original_count={Map.fetch!(@original_counts, ammo_group_id)}
|
||||
cpr={Map.get(@cprs, ammo_group_id)}
|
||||
last_used_date={Map.get(@last_used_dates, ammo_group_id)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :edit}
|
||||
return_to={Routes.container_show_path(Endpoint, :show, @container)}
|
||||
>
|
||||
<%= case @live_action do %>
|
||||
<% :edit -> %>
|
||||
<.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.ContainerLive.FormComponent}
|
||||
id={@container.id}
|
||||
@ -148,12 +165,9 @@
|
||||
return_to={Routes.container_show_path(Endpoint, :show, @container)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :edit_tags}
|
||||
return_to={Routes.container_show_path(Endpoint, :show, @container)}
|
||||
>
|
||||
</.modal>
|
||||
<% :edit_tags -> %>
|
||||
<.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.ContainerLive.EditTagsComponent}
|
||||
id={@container.id}
|
||||
@ -161,6 +175,9 @@
|
||||
action={@live_action}
|
||||
container={@container}
|
||||
return_to={Routes.container_show_path(Endpoint, :show, @container)}
|
||||
current_path={Routes.container_show_path(Endpoint, :edit_tags, @container)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
</.modal>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
|
@ -12,7 +12,6 @@ defmodule CanneryWeb.HomeLive do
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
admins = Accounts.list_users_by_role(:admin)
|
||||
socket = socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)
|
||||
{:ok, socket}
|
||||
{:ok, socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)}
|
||||
end
|
||||
end
|
||||
|
@ -17,8 +17,7 @@
|
||||
<hr class="hr" />
|
||||
|
||||
<ul class="flex flex-col space-y-4 text-center">
|
||||
<li class="flex flex-col justify-center items-center
|
||||
space-y-2">
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("Easy to Use:") %>
|
||||
</b>
|
||||
@ -37,8 +36,7 @@
|
||||
<%= gettext("Your data stays with you, period") %>
|
||||
</p>
|
||||
</li>
|
||||
<li class="flex flex-col justify-center items-center
|
||||
space-y-2">
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("Simple:") %>
|
||||
</b>
|
||||
@ -66,9 +64,13 @@
|
||||
</.link>
|
||||
<% else %>
|
||||
<div class="flex flex-wrap justify-center space-x-2">
|
||||
<a :for={%{email: email} <- @admins} class="hover:underline" href={"mailto:#{email}"}>
|
||||
<.link
|
||||
:for={%{email: email} <- @admins}
|
||||
class="hover:underline"
|
||||
href={"mailto:#{email}"}
|
||||
>
|
||||
<%= email %>
|
||||
</a>
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
</p>
|
||||
@ -77,9 +79,9 @@
|
||||
<li class="flex flex-row justify-center space-x-2">
|
||||
<b><%= gettext("Registration:") %></b>
|
||||
<p>
|
||||
<%= case Application.get_env(:cannery, Cannery.Accounts)[:registration] do
|
||||
"public" -> gettext("Public Signups")
|
||||
_ -> gettext("Invite Only")
|
||||
<%= case Accounts.registration_mode() do
|
||||
:public -> gettext("Public Signups")
|
||||
:invite_only -> gettext("Invite Only")
|
||||
end %>
|
||||
</p>
|
||||
</li>
|
||||
|
@ -18,7 +18,10 @@
|
||||
<%= changeset_errors(@changeset) %>
|
||||
</div>
|
||||
|
||||
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
|
||||
<%= label(f, :name, gettext("Name"),
|
||||
class: "title text-lg text-primary-600",
|
||||
maxlength: 255
|
||||
) %>
|
||||
<%= text_input(f, :name, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :name, "col-span-3") %>
|
||||
|
||||
|
@ -4,24 +4,13 @@ defmodule CanneryWeb.InviteLive.Index do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.{InviteCard, UserCard}
|
||||
alias Cannery.Accounts
|
||||
alias Cannery.Accounts.{Invite, Invites}
|
||||
alias CanneryWeb.HomeLive
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
|
||||
socket =
|
||||
if current_user |> Map.get(:role) == :admin do
|
||||
socket |> display_invites()
|
||||
else
|
||||
prompt = dgettext("errors", "You are not authorized to view this page")
|
||||
return_to = Routes.live_path(Endpoint, HomeLive)
|
||||
socket |> put_flash(:error, prompt) |> push_redirect(to: return_to)
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket |> display_invites()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -30,8 +19,8 @@ defmodule CanneryWeb.InviteLive.Index do
|
||||
end
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(page_title: gettext("Edit Invite"), invite: Invites.get_invite!(id, current_user))
|
||||
invite = Invites.get_invite!(id, current_user)
|
||||
socket |> assign(page_title: gettext("Edit Invite"), invite: invite)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
@ -123,22 +112,17 @@ defmodule CanneryWeb.InviteLive.Index do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("copy_to_clipboard", _params, socket) do
|
||||
prompt = dgettext("prompts", "Copied to clipboard")
|
||||
{:noreply, socket |> put_flash(:info, prompt)}
|
||||
{:noreply, socket |> put_flash(:info, dgettext("prompts", "Copied to clipboard"))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"delete_user",
|
||||
%{"id" => id},
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
) do
|
||||
%{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user)
|
||||
|
||||
prompt = dgettext("prompts", "%{user_email} deleted succesfully", user_email: user_email)
|
||||
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
|
||||
end
|
||||
|
||||
@ -151,7 +135,8 @@ defmodule CanneryWeb.InviteLive.Index do
|
||||
|> Map.get(:admin, [])
|
||||
|> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end)
|
||||
|
||||
use_counts = invites |> Invites.get_use_counts(current_user)
|
||||
users = all_users |> Map.get(:user, [])
|
||||
socket |> assign(invites: invites, admins: admins, users: users)
|
||||
socket |> assign(invites: invites, use_counts: use_counts, admins: admins, users: users)
|
||||
end
|
||||
end
|
||||
|
@ -19,13 +19,21 @@
|
||||
<% end %>
|
||||
|
||||
<div class="flex flex-col justify-center items-stretch space-y-4">
|
||||
<.invite_card :for={invite <- @invites} invite={invite} current_user={@current_user}>
|
||||
<.invite_card
|
||||
:for={invite <- @invites}
|
||||
invite={invite}
|
||||
current_user={@current_user}
|
||||
use_count={Map.get(@use_counts, invite.id)}
|
||||
>
|
||||
<:code_actions>
|
||||
<form phx-submit="copy_to_clipboard">
|
||||
<button
|
||||
type="submit"
|
||||
class="mx-2 my-1 btn btn-primary"
|
||||
phx-click={JS.dispatch("cannery:clipcopy", to: "#code-#{invite.id}")}
|
||||
aria-label={
|
||||
dgettext("actions", "Copy invite link for %{invite_name}", invite_name: invite.name)
|
||||
}
|
||||
>
|
||||
<%= dgettext("actions", "Copy to clipboard") %>
|
||||
</button>
|
||||
@ -34,7 +42,9 @@
|
||||
<.link
|
||||
patch={Routes.invite_index_path(Endpoint, :edit, invite)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{invite.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit invite for %{invite_name}", invite_name: invite.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -49,21 +59,23 @@
|
||||
invite_name: invite.name
|
||||
)
|
||||
}
|
||||
data-qa={"delete-#{invite.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete invite for %{invite_name}", invite_name: invite.name)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
|
||||
<a
|
||||
<.link
|
||||
href="#"
|
||||
class="btn btn-primary"
|
||||
phx-click={if invite.disabled_at, do: "enable_invite", else: "disable_invite"}
|
||||
phx-value-id={invite.id}
|
||||
>
|
||||
<%= if invite.disabled_at, do: gettext("Enable"), else: gettext("Disable") %>
|
||||
</a>
|
||||
</.link>
|
||||
|
||||
<a
|
||||
<.link
|
||||
:if={invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil())}
|
||||
href="#"
|
||||
class="btn btn-primary"
|
||||
@ -76,7 +88,7 @@
|
||||
}
|
||||
>
|
||||
<%= dgettext("actions", "Set Unlimited") %>
|
||||
</a>
|
||||
</.link>
|
||||
</.invite_card>
|
||||
</div>
|
||||
|
||||
|
@ -1,128 +0,0 @@
|
||||
defmodule CanneryWeb.LiveHelpers do
|
||||
@moduledoc """
|
||||
Contains common helper functions for liveviews
|
||||
"""
|
||||
|
||||
import Phoenix.Component
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
Renders a live component inside a modal.
|
||||
|
||||
The rendered modal receives a `:return_to` option to properly update
|
||||
the URL when the modal is closed.
|
||||
|
||||
## Examples
|
||||
|
||||
<.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
|
||||
id={@<%= schema.singular %>.id || :new}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}
|
||||
<%= schema.singular %>: @<%= schema.singular %>
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
def modal(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
patch={@return_to}
|
||||
id="modal-bg"
|
||||
class="fade-in fixed z-10 left-0 top-0
|
||||
w-full h-full overflow-hidden
|
||||
p-8 flex flex-col justify-center items-center cursor-auto"
|
||||
style="background-color: rgba(0,0,0,0.4);"
|
||||
phx-remove={hide_modal()}
|
||||
>
|
||||
<span class="hidden"></span>
|
||||
</.link>
|
||||
|
||||
<div
|
||||
id="modal"
|
||||
class="fixed z-10 left-0 top-0 pointer-events-none
|
||||
w-full h-full overflow-hidden
|
||||
p-4 sm:p-8 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div
|
||||
id="modal-content"
|
||||
class="fade-in-scale w-full max-w-3xl relative
|
||||
pointer-events-auto overflow-hidden
|
||||
px-8 py-4 sm:py-8
|
||||
flex flex-col justify-start items-center
|
||||
bg-white border-2 rounded-lg"
|
||||
>
|
||||
<.link
|
||||
patch={@return_to}
|
||||
id="close"
|
||||
class="absolute top-8 right-10
|
||||
text-gray-500 hover:text-gray-800
|
||||
transition-all duration-500 ease-in-out"
|
||||
phx-remove={hide_modal()}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-times"></i>
|
||||
</.link>
|
||||
|
||||
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp hide_modal(js \\ %JS{}) do
|
||||
js
|
||||
|> JS.hide(to: "#modal", transition: "fade-out")
|
||||
|> JS.hide(to: "#modal-bg", transition: "fade-out")
|
||||
|> JS.hide(to: "#modal-content", transition: "fade-out-scale")
|
||||
end
|
||||
|
||||
@doc """
|
||||
A toggle button element that can be directed to a liveview or a
|
||||
live_component's `handle_event/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.toggle_button action="my_liveview_action" value={@some_value}>
|
||||
<span>Toggle me!</span>
|
||||
</.toggle_button>
|
||||
<.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
|
||||
<span>Whatever you want</span>
|
||||
</.toggle_button>
|
||||
"""
|
||||
def toggle_button(assigns) do
|
||||
assigns = assigns |> assign_new(:id, fn -> assigns.action end)
|
||||
|
||||
~H"""
|
||||
<label for={@id} class="inline-flex relative items-center cursor-pointer">
|
||||
<input
|
||||
id={@id}
|
||||
type="checkbox"
|
||||
value={@value}
|
||||
checked={@value}
|
||||
class="sr-only peer"
|
||||
data-qa={@id}
|
||||
{
|
||||
if assigns |> Map.has_key?(:target),
|
||||
do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
|
||||
else: %{"phx-click": @action, "phx-value-value": @value}
|
||||
}
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-300 rounded-full peer
|
||||
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
|
||||
peer-checked:bg-gray-600
|
||||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||
after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
|
||||
after:border after:rounded-full after:h-5 after:w-5
|
||||
after:transition-all after:duration-250 after:ease-in-out
|
||||
transition-colors duration-250 ease-in-out">
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</span>
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
end
|
@ -5,8 +5,12 @@ defmodule CanneryWeb.RangeLive.FormComponent do
|
||||
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup}
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@impl true
|
||||
def mount(socket), do: {:ok, socket |> assign(:ammo_group, nil)}
|
||||
|
||||
@impl true
|
||||
@spec update(
|
||||
%{
|
||||
@ -19,28 +23,23 @@ defmodule CanneryWeb.RangeLive.FormComponent do
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(
|
||||
%{
|
||||
shot_group: %ShotGroup{ammo_group_id: ammo_group_id} = shot_group,
|
||||
shot_group: %ShotGroup{ammo_group_id: ammo_group_id},
|
||||
current_user: current_user
|
||||
} = assigns,
|
||||
socket
|
||||
) do
|
||||
changeset = shot_group |> ShotGroup.update_changeset(current_user, %{})
|
||||
)
|
||||
when is_binary(ammo_group_id) do
|
||||
ammo_group = Ammo.get_ammo_group!(ammo_group_id, current_user)
|
||||
{:ok, socket |> assign(assigns) |> assign(ammo_group: ammo_group, changeset: changeset)}
|
||||
{:ok, socket |> assign(assigns) |> assign(:ammo_group, ammo_group) |> assign_changeset(%{})}
|
||||
end
|
||||
|
||||
def update(%{shot_group: %ShotGroup{}} = assigns, socket) do
|
||||
{:ok, socket |> assign(assigns) |> assign_changeset(%{})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"validate",
|
||||
%{"shot_group" => shot_group_params},
|
||||
%{assigns: %{current_user: current_user, shot_group: shot_group}} = socket
|
||||
) do
|
||||
changeset =
|
||||
shot_group
|
||||
|> ShotGroup.update_changeset(current_user, shot_group_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
def handle_event("validate", %{"shot_group" => shot_group_params}, socket) do
|
||||
{:noreply, socket |> assign_changeset(shot_group_params, :validate)}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
@ -61,4 +60,37 @@ defmodule CanneryWeb.RangeLive.FormComponent do
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp assign_changeset(
|
||||
%{
|
||||
assigns: %{
|
||||
action: live_action,
|
||||
current_user: user,
|
||||
ammo_group: ammo_group,
|
||||
shot_group: shot_group
|
||||
}
|
||||
} = socket,
|
||||
shot_group_params,
|
||||
action \\ nil
|
||||
) do
|
||||
default_action =
|
||||
case live_action do
|
||||
:add_shot_group -> :insert
|
||||
editing when editing in [:edit, :edit_shot_group] -> :update
|
||||
end
|
||||
|
||||
changeset =
|
||||
case default_action do
|
||||
:insert -> shot_group |> ShotGroup.create_changeset(user, ammo_group, shot_group_params)
|
||||
:update -> shot_group |> ShotGroup.update_changeset(user, shot_group_params)
|
||||
end
|
||||
|
||||
changeset =
|
||||
case changeset |> Changeset.apply_action(action || default_action) do
|
||||
{:ok, _data} -> changeset
|
||||
{:error, changeset} -> changeset
|
||||
end
|
||||
|
||||
socket |> assign(:changeset, changeset)
|
||||
end
|
||||
end
|
||||
|
@ -29,8 +29,12 @@
|
||||
|
||||
<%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
|
||||
<%= textarea(f, :notes,
|
||||
id: "shot-group-form-notes",
|
||||
class: "input input-primary col-span-2",
|
||||
phx_hook: "MaintainAttrs"
|
||||
maxlength: 255,
|
||||
placeholder: gettext("Really great weather"),
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore"
|
||||
) %>
|
||||
<%= error_tag(f, :notes, "col-span-3") %>
|
||||
|
||||
|
@ -4,18 +4,17 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.AmmoGroupCard
|
||||
alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Repo}
|
||||
alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@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
|
||||
@ -81,7 +80,6 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"toggle_staged",
|
||||
%{"ammo_group_id" => ammo_group_id},
|
||||
@ -96,7 +94,6 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :index))}
|
||||
end
|
||||
@ -105,18 +102,39 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
{:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :search, search_term))}
|
||||
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)
|
||||
|> Repo.preload(ammo_group: :ammo_type)
|
||||
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: %{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)
|
||||
cprs = ammo_groups |> Ammo.get_cprs(current_user)
|
||||
last_used_dates = ammo_groups |> ActivityLog.get_last_used_dates(current_user)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
ammo_groups: ammo_groups,
|
||||
original_counts: original_counts,
|
||||
cprs: cprs,
|
||||
last_used_dates: last_used_dates,
|
||||
chart_data: chart_data,
|
||||
shot_groups: shot_groups
|
||||
)
|
||||
@ -125,7 +143,6 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
@spec get_chart_data_for_shot_group([ShotGroup.t()]) :: [map()]
|
||||
defp get_chart_data_for_shot_group(shot_groups) do
|
||||
shot_groups
|
||||
|> Repo.preload(ammo_group: :ammo_type)
|
||||
|> Enum.group_by(fn %{date: date} -> date end, fn %{count: count} -> count end)
|
||||
|> Enum.map(fn {date, rounds} ->
|
||||
sum = Enum.sum(rounds)
|
||||
|
@ -18,7 +18,14 @@
|
||||
</.link>
|
||||
|
||||
<div class="w-full flex flex-row flex-wrap justify-center items-stretch">
|
||||
<.ammo_group_card :for={ammo_group <- @ammo_groups} ammo_group={ammo_group}>
|
||||
<.ammo_group_card
|
||||
:for={%{id: ammo_group_id} = ammo_group <- @ammo_groups}
|
||||
ammo_group={ammo_group}
|
||||
original_count={Map.fetch!(@original_counts, ammo_group_id)}
|
||||
cpr={Map.get(@cprs, ammo_group_id)}
|
||||
last_used_date={Map.get(@last_used_dates, ammo_group_id)}
|
||||
current_user={@current_user}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@ -67,18 +74,43 @@
|
||||
<%= 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={:search}
|
||||
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"
|
||||
data-qa="shot_group_search"
|
||||
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,
|
||||
placeholder: gettext("Search shot records")
|
||||
) %>
|
||||
@ -102,7 +134,11 @@
|
||||
<.link
|
||||
patch={Routes.range_index_path(Endpoint, :edit, shot_group)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{shot_group.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Edit shot record of %{shot_group_count} shots",
|
||||
shot_group_count: shot_group.count
|
||||
)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -115,7 +151,11 @@
|
||||
data-confirm={
|
||||
dgettext("prompts", "Are you sure you want to delete this shot record?")
|
||||
}
|
||||
data-qa={"delete-#{shot_group.id}"}
|
||||
aria-label={
|
||||
dgettext("actions", "Delete shot record of %{shot_group_count} shots",
|
||||
shot_group_count: shot_group.count
|
||||
)
|
||||
}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||
</.link>
|
||||
@ -126,7 +166,9 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.modal :if={@live_action == :edit} return_to={Routes.range_index_path(Endpoint, :index)}>
|
||||
<%= case @live_action do %>
|
||||
<% :edit -> %>
|
||||
<.modal return_to={Routes.range_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.RangeLive.FormComponent}
|
||||
id={@shot_group.id}
|
||||
@ -136,12 +178,9 @@
|
||||
return_to={Routes.range_index_path(Endpoint, :index)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :add_shot_group}
|
||||
return_to={Routes.range_index_path(Endpoint, :index)}
|
||||
>
|
||||
</.modal>
|
||||
<% :add_shot_group -> %>
|
||||
<.modal return_to={Routes.range_index_path(Endpoint, :index)}>
|
||||
<.live_component
|
||||
module={CanneryWeb.Components.AddShotGroupComponent}
|
||||
id={:new}
|
||||
@ -151,4 +190,6 @@
|
||||
return_to={Routes.range_index_path(Endpoint, :index)}
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</.modal>
|
||||
</.modal>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
|
@ -1,11 +1,10 @@
|
||||
defmodule CanneryWeb.TagLive.FormComponent do
|
||||
@moduledoc """
|
||||
Livecomponent that can update or create an Cannery.Tags.Tag
|
||||
Livecomponent that can update or create an Cannery.Containers.Tag
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_component
|
||||
alias Cannery.Tags
|
||||
alias Cannery.{Accounts.User, Tags.Tag}
|
||||
alias Cannery.{Accounts.User, Containers, Containers.Tag}
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@ -56,7 +55,7 @@ defmodule CanneryWeb.TagLive.FormComponent do
|
||||
tag_params
|
||||
) do
|
||||
socket =
|
||||
case Tags.update_tag(tag, tag_params, current_user) do
|
||||
case Containers.update_tag(tag, tag_params, current_user) do
|
||||
{:ok, %{name: tag_name}} ->
|
||||
prompt = dgettext("prompts", "%{name} updated successfully", name: tag_name)
|
||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||
@ -74,7 +73,7 @@ defmodule CanneryWeb.TagLive.FormComponent do
|
||||
tag_params
|
||||
) do
|
||||
socket =
|
||||
case Tags.create_tag(tag_params, current_user) do
|
||||
case Containers.create_tag(tag_params, current_user) do
|
||||
{:ok, %{name: tag_name}} ->
|
||||
prompt = dgettext("prompts", "%{name} created successfully", name: tag_name)
|
||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||
|
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
|
||||
<%= text_input(f, :name, class: "input input-primary col-span-2") %>
|
||||
<%= text_input(f, :name, class: "input input-primary col-span-2", maxlength: 255) %>
|
||||
<%= error_tag(f, :name, "col-span-3") %>
|
||||
|
||||
<%= label(f, :bg_color, gettext("Background color"), class: "title text-lg text-primary-600") %>
|
||||
|
@ -1,11 +1,10 @@
|
||||
defmodule CanneryWeb.TagLive.Index do
|
||||
@moduledoc """
|
||||
Liveview to show a Cannery.Tags.Tag index
|
||||
Liveview to show a Cannery.Containers.Tag index
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.TagCard
|
||||
alias Cannery.{Tags, Tags.Tag}
|
||||
alias Cannery.{Containers, Containers.Tag}
|
||||
alias CanneryWeb.ViewHelpers
|
||||
|
||||
@impl true
|
||||
@ -26,7 +25,7 @@ defmodule CanneryWeb.TagLive.Index do
|
||||
socket
|
||||
|> assign(
|
||||
page_title: gettext("Edit Tag"),
|
||||
tag: Tags.get_tag!(id, current_user)
|
||||
tag: Containers.get_tag!(id, current_user)
|
||||
)
|
||||
end
|
||||
|
||||
@ -60,12 +59,13 @@ defmodule CanneryWeb.TagLive.Index do
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
|
||||
%{name: tag_name} = Tags.get_tag!(id, current_user) |> Tags.delete_tag!(current_user)
|
||||
%{name: tag_name} =
|
||||
Containers.get_tag!(id, current_user) |> Containers.delete_tag!(current_user)
|
||||
|
||||
prompt = dgettext("prompts", "%{name} deleted succesfully", name: tag_name)
|
||||
{:noreply, socket |> put_flash(:info, prompt) |> display_tags()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.tag_index_path(Endpoint, :index))}
|
||||
end
|
||||
@ -75,6 +75,6 @@ defmodule CanneryWeb.TagLive.Index do
|
||||
end
|
||||
|
||||
defp display_tags(%{assigns: %{search: search, current_user: current_user}} = socket) do
|
||||
socket |> assign(tags: Tags.list_tags(search, current_user))
|
||||
socket |> assign(tags: Containers.list_tags(search, current_user))
|
||||
end
|
||||
end
|
||||
|
@ -18,20 +18,20 @@
|
||||
<.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">
|
||||
<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={:search}
|
||||
for={%{}}
|
||||
as={:search}
|
||||
phx-change="search"
|
||||
phx-submit="search"
|
||||
class="grow self-stretch flex flex-col items-stretch"
|
||||
data-qa="tag_search"
|
||||
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,
|
||||
placeholder: gettext("Search tags")
|
||||
) %>
|
||||
@ -49,7 +49,7 @@
|
||||
<.link
|
||||
patch={Routes.tag_index_path(Endpoint, :edit, tag)}
|
||||
class="text-primary-600 link"
|
||||
data-qa={"edit-#{tag.id}"}
|
||||
aria-label={dgettext("actions", "Edit %{tag_name}", tag_name: tag.name)}
|
||||
>
|
||||
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||
</.link>
|
||||
@ -62,13 +62,14 @@
|
||||
data-confirm={
|
||||
dgettext("prompts", "Are you sure you want to delete %{name}?", name: tag.name)
|
||||
}
|
||||
data-qa={"delete-#{tag.id}"}
|
||||
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>
|
||||
|
||||
<.modal :if={@live_action in [:new, :edit]} return_to={Routes.tag_index_path(Endpoint, :index)}>
|
||||
|
@ -77,7 +77,6 @@ defmodule CanneryWeb.Router do
|
||||
|
||||
live "/type/:id", AmmoTypeLive.Show, :show
|
||||
live "/type/:id/edit", AmmoTypeLive.Show, :edit
|
||||
live "/type/:id/table", AmmoTypeLive.Show, :table
|
||||
|
||||
live "/containers", ContainerLive.Index, :index
|
||||
live "/containers/new", ContainerLive.Index, :new
|
||||
|
@ -24,9 +24,12 @@
|
||||
|
||||
<hr class="w-full hr" />
|
||||
|
||||
<a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-600 text-lg">
|
||||
<.link
|
||||
href={Routes.live_path(Endpoint, HomeLive)}
|
||||
class="link title text-primary-600 text-lg"
|
||||
>
|
||||
<%= dgettext("errors", "Go back home") %>
|
||||
</a>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={:user}
|
||||
for={%{}}
|
||||
as={:user}
|
||||
action={Routes.user_confirmation_path(@conn, :create)}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={:user}
|
||||
for={%{}}
|
||||
as={:user}
|
||||
action={Routes.user_reset_password_path(@conn, :create)}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
|
@ -107,9 +107,9 @@
|
||||
action={Routes.user_settings_path(@conn, :update)}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<h3 class="title text-primary-600 text-lg text-center col-span-3">
|
||||
<%= dgettext("actions", "Change Language") %>
|
||||
</h3>
|
||||
<%= label(f, :locale, dgettext("actions", "Change Language"),
|
||||
class: "title text-primary-600 text-lg text-center col-span-3"
|
||||
) %>
|
||||
|
||||
<div
|
||||
:if={@locale_changeset.action && not @locale_changeset.valid?()}
|
||||
|
@ -1,6 +1,5 @@
|
||||
defmodule CanneryWeb.ErrorView do
|
||||
use CanneryWeb, :view
|
||||
import CanneryWeb.Components.Topbar
|
||||
alias CanneryWeb.HomeLive
|
||||
|
||||
def template_not_found(error_path, _assigns) do
|
||||
|
@ -1,6 +1,5 @@
|
||||
defmodule CanneryWeb.LayoutView do
|
||||
use CanneryWeb, :view
|
||||
import CanneryWeb.Components.Topbar
|
||||
alias CanneryWeb.HomeLive
|
||||
|
||||
# Phoenix LiveDashboard is available only in development by default,
|
||||
|
@ -7,61 +7,6 @@ defmodule CanneryWeb.ViewHelpers do
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
@doc """
|
||||
Phoenix.Component for a <time> element that renders the naivedatetime in the
|
||||
user's local timezone with Alpine.js
|
||||
"""
|
||||
|
||||
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
|
||||
|
||||
def datetime(assigns) do
|
||||
~H"""
|
||||
<time
|
||||
:if={@datetime}
|
||||
datetime={cast_datetime(@datetime)}
|
||||
x-data={"{
|
||||
datetime:
|
||||
Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
|
||||
.format(new Date(\"#{cast_datetime(@datetime)}\"))
|
||||
}"}
|
||||
x-text="datetime"
|
||||
>
|
||||
<%= cast_datetime(@datetime) %>
|
||||
</time>
|
||||
"""
|
||||
end
|
||||
|
||||
@spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
|
||||
defp cast_datetime(%NaiveDateTime{} = datetime) do
|
||||
datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
|
||||
end
|
||||
|
||||
defp cast_datetime(_datetime), do: ""
|
||||
|
||||
@doc """
|
||||
Phoenix.Component for a <date> element that renders the Date in the user's
|
||||
local timezone with Alpine.js
|
||||
"""
|
||||
|
||||
attr :date, :any, required: true, doc: "A `Date` struct or nil"
|
||||
|
||||
def date(assigns) do
|
||||
~H"""
|
||||
<time
|
||||
:if={@date}
|
||||
datetime={@date |> Date.to_iso8601(:extended)}
|
||||
x-data={"{
|
||||
date:
|
||||
Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'})
|
||||
.format(new Date(\"#{@date |> Date.to_iso8601(:extended)}\"))
|
||||
}"}
|
||||
x-text="date"
|
||||
>
|
||||
<%= @date |> Date.to_iso8601(:extended) %>
|
||||
</time>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Displays emoji as text emoji if SHIBAO_MODE is set to true :)
|
||||
"""
|
||||
@ -87,23 +32,6 @@ defmodule CanneryWeb.ViewHelpers do
|
||||
"data:image/png;base64," <> img_data
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a downloadable QR Code element
|
||||
"""
|
||||
|
||||
attr :content, :string, required: true
|
||||
attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
|
||||
attr :image_class, :string, default: "w-64 h-max"
|
||||
attr :width, :integer, default: 384, doc: "width of png to generate"
|
||||
|
||||
def qr_code(assigns) do
|
||||
~H"""
|
||||
<a href={qr_code_image(@content)} download={@filename <> ".png"}>
|
||||
<img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a random color in `#ffffff` hex format
|
||||
|
||||
|
6
mix.exs
6
mix.exs
@ -4,7 +4,7 @@ defmodule Cannery.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :cannery,
|
||||
version: "0.8.3",
|
||||
version: "0.9.0",
|
||||
elixir: "1.14.1",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: Mix.compilers(),
|
||||
@ -47,13 +47,13 @@ defmodule Cannery.MixProject do
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:bcrypt_elixir, "~> 2.0"},
|
||||
{:bcrypt_elixir, "~> 3.0"},
|
||||
{:phoenix, "~> 1.6.0"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.18.0"},
|
||||
{:phoenix_view, "~> 1.1"},
|
||||
{:phoenix_view, "~> 2.0"},
|
||||
{:phoenix_live_dashboard, "~> 0.6"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
|
33
mix.lock
33
mix.lock
@ -1,5 +1,5 @@
|
||||
%{
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||
@ -11,38 +11,39 @@
|
||||
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
|
||||
"ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"},
|
||||
"eqrcode": {:hex, :eqrcode, "0.1.10", "6294fece9d68ad64eef1c3c92cf111cfd6469f4fbf230a2d4cc905a682178f3f", [:mix], [], "hexpm", "da30e373c36a0fd37ab6f58664b16029919896d6c45a68a95cc4d713e81076f1"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
|
||||
"expo": {:hex, :expo, "0.3.0", "13127c1d5f653b2927f2616a4c9ace5ae372efd67c7c2693b87fd0fdc30c6feb", [:mix], [], "hexpm", "fb3cd4bf012a77bc1608915497dae2ff684a06f0fa633c7afa90c4d72b881823"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.2", "dfa97532ba66910b2a3016a4bbd796f41a86fc71dd5227e96f4c8581fdf0fdf0", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6b5d7139eda18a753e3250e27e4a929f8d2c880dd0d460cb9986305dea3e03af"},
|
||||
"expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
|
||||
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gettext": {:hex, :gettext, "0.22.0", "a25d71ec21b1848957d9207b81fd61cb25161688d282d58bdafef74c2270bdc4", [:mix], [{:expo, "~> 0.3.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "cb0675141576f73720c8e49b4f0fd3f2c69f0cd8c218202724d4aebab8c70ace"},
|
||||
"gettext": {:hex, :gettext, "0.22.1", "e7942988383c3d9eed4bdc22fc63e712b655ae94a672a27e4900e3d4a2c43581", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "ad105b8dab668ee3f90c0d3d94ba75e9aead27a62495c101d94f2657a190ac5d"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"oban": {:hex, :oban, "2.13.6", "a0cb1bce3bd393770512231fb5a3695fa19fd3af10d7575bf73f837aee7abf43", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c1c5eb16f377b3cbbf2ea14be24d20e3d91285af9d1ac86260b7c2af5464887"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
|
||||
"oban": {:hex, :oban, "2.14.2", "ae925d9a33e110addaa59ff7ec1b2fd84270ac7eb00fbb4b4a179d74c407bba3", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "32bf30127c8c44ac42f05f229a50fadc2177b3e799c29499f5daf90d5e5b5d3c"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.1", "b0bf8f3348dec4910907a2ad1453e642f6fe4d444376c1c9b26222d63c73cf97", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "b6c5d744bf4b40692b1b361d3608bdfd05aeab83e17c7bc217d730f007f31abf"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.2", "635cf07de947235deb030cd6b776c71a3b790ab04cebf526aa8c879fe17c7784", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "da287a77327e996cc166e4c440c3ad5ab33ccdb151b91c793209b39ebbce5b75"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.1.0", "f8e4780705c9f254cc853f7a40e25f7198ba4d91102bcfad2226669b69766b35", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "aa82f10afd9a4b6080fdf3274dbb9432b25b210d42b4b6b55308f6e59cd87c3d"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
||||
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
|
||||
|
@ -12,12 +12,12 @@ msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.ex:54
|
||||
#: lib/cannery_web/live/ammo_group_live/index.ex:62
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:41
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add Ammo"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:37
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:34
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add your first box!"
|
||||
msgstr ""
|
||||
@ -66,11 +66,11 @@ msgstr ""
|
||||
msgid "Invite someone new!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/topbar.ex:137
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:31
|
||||
#: lib/cannery_web/components/core_components/topbar.html.heex:124
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:32
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:44
|
||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:45
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:31
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:32
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
@ -97,19 +97,19 @@ msgstr ""
|
||||
msgid "New Tag"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/topbar.ex:129
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:28
|
||||
#: lib/cannery_web/components/core_components/topbar.html.heex:116
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:29
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:37
|
||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:42
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:28
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:29
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:15
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Resend confirmation instructions"
|
||||
msgstr ""
|
||||
@ -120,28 +120,28 @@ msgstr ""
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:54
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:82
|
||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:157
|
||||
#: lib/cannery_web/live/container_live/form_component.html.heex:51
|
||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:32
|
||||
#: lib/cannery_web/live/range_live/form_component.html.heex:41
|
||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:57
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:84
|
||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:350
|
||||
#: lib/cannery_web/live/container_live/form_component.html.heex:57
|
||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:35
|
||||
#: lib/cannery_web/live/range_live/form_component.html.heex:45
|
||||
#: lib/cannery_web/live/tag_live/form_component.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:15
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Send instructions to reset password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:76
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:68
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Why not add one?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:50
|
||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
@ -156,9 +156,9 @@ msgstr ""
|
||||
msgid "Why not get some ready to shoot?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:101
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:101
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:38
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:125
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:103
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:45
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Record shots"
|
||||
msgstr ""
|
||||
@ -168,32 +168,27 @@ msgstr ""
|
||||
msgid "Add another container!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:94
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Move containers"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/move_ammo_group_component.ex:126
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:30
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Copy to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:22
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "add a container first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:75
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:77
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:111
|
||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:110
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Change Language"
|
||||
msgstr ""
|
||||
@ -203,34 +198,36 @@ msgstr ""
|
||||
msgid "Change language"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:60
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "View in Catalog"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:32
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "add an ammo type first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/move_ammo_group_component.ex:80
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:142
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:96
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Move ammo"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:78
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:90
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set Unlimited"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:86
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:31
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:89
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Stage for range"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:85
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:30
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:88
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unstage from range"
|
||||
msgstr ""
|
||||
@ -239,3 +236,125 @@ msgstr ""
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Export Data as JSON"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:110
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Clone %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:87
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:145
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Clone %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Copy invite link for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:129
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:104
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:162
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:48
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/tag_live/index.html.heex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{tag_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete invite for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.ex:161
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:155
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete shot record of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:100
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:19
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:77
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:135
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/tag_live/index.html.heex:52
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{tag_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:164
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:46
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit invite for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.ex:146
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit shot group of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:138
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit shot record of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:118
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Stage"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:65
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:124
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Tag %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:117
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unstage"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:90
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "View %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:176
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Clone ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:191
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:76
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:152
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:206
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "View ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
@ -25,12 +25,12 @@ msgstr ""
|
||||
## effect: edit them in PO (.po) files instead.
|
||||
#: lib/cannery_web/live/ammo_group_live/index.ex:54
|
||||
#: lib/cannery_web/live/ammo_group_live/index.ex:62
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:41
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add Ammo"
|
||||
msgstr "Munition hinzufügen"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:37
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:34
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add your first box!"
|
||||
msgstr "Fügen Sie ihre erste Box hinzu!"
|
||||
@ -79,11 +79,11 @@ msgstr "Passwort vergessen?"
|
||||
msgid "Invite someone new!"
|
||||
msgstr "Laden Sie jemanden ein!"
|
||||
|
||||
#: lib/cannery_web/components/topbar.ex:137
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:31
|
||||
#: lib/cannery_web/components/core_components/topbar.html.heex:124
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:32
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:44
|
||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:45
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:31
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:32
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
@ -110,19 +110,19 @@ msgstr "Neuer Behälter"
|
||||
msgid "New Tag"
|
||||
msgstr "Neuer Tag"
|
||||
|
||||
#: lib/cannery_web/components/topbar.ex:129
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:28
|
||||
#: lib/cannery_web/components/core_components/topbar.html.heex:116
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:29
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_registration/new.html.heex:37
|
||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:42
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:28
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:29
|
||||
#: lib/cannery_web/templates/user_session/new.html.heex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Register"
|
||||
msgstr "Registrieren"
|
||||
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:3
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:15
|
||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Resend confirmation instructions"
|
||||
msgstr "Bestätigungsmail erneut senden"
|
||||
@ -133,28 +133,28 @@ msgstr "Bestätigungsmail erneut senden"
|
||||
msgid "Reset password"
|
||||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:54
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:82
|
||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:157
|
||||
#: lib/cannery_web/live/container_live/form_component.html.heex:51
|
||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:32
|
||||
#: lib/cannery_web/live/range_live/form_component.html.heex:41
|
||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:57
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:84
|
||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:350
|
||||
#: lib/cannery_web/live/container_live/form_component.html.heex:57
|
||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:35
|
||||
#: lib/cannery_web/live/range_live/form_component.html.heex:45
|
||||
#: lib/cannery_web/live/tag_live/form_component.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:15
|
||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Send instructions to reset password"
|
||||
msgstr "Anleitung zum Passwort zurücksetzen zusenden"
|
||||
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:76
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:68
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Why not add one?"
|
||||
msgstr "Warum fügen Sie keine hinzu?"
|
||||
|
||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:50
|
||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Add"
|
||||
msgstr "Hinzufügen"
|
||||
@ -169,9 +169,9 @@ msgstr "Munition markieren"
|
||||
msgid "Why not get some ready to shoot?"
|
||||
msgstr "Warum nicht einige für den Schießstand auswählen?"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:101
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:101
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:38
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:125
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:103
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:45
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Record shots"
|
||||
msgstr "Schüsse dokumentieren"
|
||||
@ -181,32 +181,27 @@ msgstr "Schüsse dokumentieren"
|
||||
msgid "Add another container!"
|
||||
msgstr "Einen weiteren Behälter hinzufügen!"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:94
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Move containers"
|
||||
msgstr "Behälter verschieben"
|
||||
|
||||
#: lib/cannery_web/components/move_ammo_group_component.ex:126
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select"
|
||||
msgstr "Markieren"
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:30
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Copy to clipboard"
|
||||
msgstr "In die Zwischenablage kopieren"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:22
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "add a container first"
|
||||
msgstr "Zuerst einen Behälter hinzufügen"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:75
|
||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:77
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create"
|
||||
msgstr "Erstellen"
|
||||
|
||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:111
|
||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:110
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Change Language"
|
||||
msgstr "Sprache wechseln"
|
||||
@ -216,34 +211,36 @@ msgstr "Sprache wechseln"
|
||||
msgid "Change language"
|
||||
msgstr "Sprache wechseln"
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:60
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "View in Catalog"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:32
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "add an ammo type first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/components/move_ammo_group_component.ex:80
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:142
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:96
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Move ammo"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:78
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:90
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set Unlimited"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:86
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:31
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:89
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Stage for range"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:85
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:30
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:88
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unstage from range"
|
||||
msgstr ""
|
||||
@ -252,3 +249,125 @@ msgstr ""
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Export Data as JSON"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:110
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Clone %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:87
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:145
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Clone %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Copy invite link for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:129
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:104
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:162
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:48
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/tag_live/index.html.heex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete %{tag_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete invite for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.ex:161
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:155
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete shot record of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:100
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:19
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:77
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:135
|
||||
#: lib/cannery_web/live/container_live/show.html.heex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/tag_live/index.html.heex:52
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit %{tag_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:164
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/invite_live/index.html.heex:46
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit invite for %{invite_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/show.ex:146
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit shot group of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/range_live/index.html.heex:138
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit shot record of %{shot_group_count} shots"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:118
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Stage"
|
||||
msgstr "Munition markieren"
|
||||
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:65
|
||||
#: lib/cannery_web/live/container_live/index.html.heex:124
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Tag %{container_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:117
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unstage"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:90
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "View %{ammo_type_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:176
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Clone ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:191
|
||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:76
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Delete ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:152
|
||||
#: lib/cannery_web/live/ammo_type_live/show.html.heex:206
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "View ammo group of %{ammo_group_count} bullets"
|
||||
msgstr ""
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user