Compare commits
	
		
			158 Commits
		
	
	
		
			0.5.1
			...
			09d3754f92
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 09d3754f92 | |||
| fa67fd5a3b | |||
| 0a56b32b17 | |||
| 5f214ab7f9 | |||
| 0c95fa88f4 | |||
| 61f8776d2e | |||
| 26f75c7d15 | |||
| 23f7af2a93 | |||
| 8e771a2d9a | |||
| 16155687c9 | |||
| ef0d644ee3 | |||
| faacf80b51 | |||
| 7a7754767f | |||
| 0894293620 | |||
| 58b32817ba | |||
| 45da547f62 | |||
| 3ea4b77b67 | |||
| d743336868 | |||
| c99fbabcaa | |||
| 95642061db | |||
| 11ef53d1bf | |||
| 757eca47f7 | |||
| 7191fe8e4b | |||
| 
						 | 
					02253c74ac | ||
| 
						 | 
					fa32b2a073 | ||
| 
						 | 
					aa0ea10fa4 | ||
| 
						 | 
					72ac611e15 | ||
| 
						 | 
					ff0af84d5d | ||
| 
						 | 
					851d0954ad | ||
| 
						 | 
					ea4336adc5 | ||
| 
						 | 
					fcce6a6f40 | ||
| 
						 | 
					4d3085da2a | ||
| 2ec60ba342 | |||
| e0ac714681 | |||
| 20a2311229 | |||
| a6ae8a872d | |||
| 81e448afc4 | |||
| dfebad713f | |||
| bf0ea4168b | |||
| 5ffa627beb | |||
| 25d4f1916a | |||
| fd583f2941 | |||
| 5cae646758 | |||
| d389515f76 | |||
| c2568b6521 | |||
| 60462877c5 | |||
| ba5a4e69b2 | |||
| 
						 | 
					ba36b4e4c5 | ||
| 
						 | 
					f5a092e91f | ||
| 09774e0aed | |||
| 44d4d7c6f9 | |||
| 1fed895b82 | |||
| 3480aff61d | |||
| 1ea4b99c81 | |||
| 2191e7f4f4 | |||
| 8231683958 | |||
| a3e7e3a86d | |||
| 0c088ed06d | |||
| 1c7721887f | |||
| 0dbd1af553 | |||
| c828def2b2 | |||
| 7ef582510e | |||
| 726ede7e46 | |||
| 8fa75e2559 | |||
| 0d6f6de7df | |||
| 76d3554b4b | |||
| 09394ea408 | |||
| 3dd6430105 | |||
| 53bb0ddb03 | |||
| e3ff04ced3 | |||
| db4b3fab24 | |||
| d86f5a6d98 | |||
| 9420337228 | |||
| e2a4dc4b92 | |||
| 175eef95fb | |||
| ba8d7988b3 | |||
| 1f017ced4a | |||
| 67304ae22e | |||
| 30da5bc4f7 | |||
| 580d703f12 | |||
| cb4de9a6ff | |||
| ca4b5b9822 | |||
| 13bf8ab2b1 | |||
| ffd1029e78 | |||
| 1d8f3360d8 | |||
| 6b565abf93 | |||
| 239b15a782 | |||
| cfbec3189c | |||
| cc31958bbe | |||
| 9e386f1631 | |||
| 36a0a1c6c8 | |||
| 2e0bb861f1 | |||
| dbc575d305 | |||
| 5390dcc4c2 | |||
| dc355fcd8e | |||
| 44fbd69e0f | |||
| 10c4f40864 | |||
| d512ec8e10 | |||
| 81b68ba4af | |||
| b6e0a543ec | |||
| cf75007e4d | |||
| 1583ad3503 | |||
| 2af9c7a4f9 | |||
| 7e7d6258d5 | |||
| f246b9db93 | |||
| 5836a82ff7 | |||
| 7464947497 | |||
| 3adb8c9aae | |||
| dce04e4d7f | |||
| ee6266be3f | |||
| 36f56528ee | |||
| 76bbab1de9 | |||
| 6c09261368 | |||
| 3593334c85 | |||
| 57b5cb432d | |||
| 5b5fd7173b | |||
| 41bcc2f456 | |||
| 947659b207 | |||
| 9ebca20dc6 | |||
| 3cbd62e84c | |||
| 67010640f0 | |||
| 5fd46c326f | |||
| 8c62a39c97 | |||
| 9a1a104c6d | |||
| 11b1ae9980 | |||
| 948fa929ec | |||
| 268085e761 | |||
| a1efd7cc60 | |||
| 2db9ab968f | |||
| 7863b06215 | |||
| eb8cae27c0 | |||
| ecca9ee4a9 | |||
| 
						 | 
					a0c4f20afd | ||
| 
						 | 
					0e46a05360 | ||
| 
						 | 
					1062d97f79 | ||
| 
						 | 
					1b5df8c56c | ||
| 
						 | 
					da5c136313 | ||
| ef206d273f | |||
| 
						 | 
					ef513a8108 | ||
| 
						 | 
					ed1765c652 | ||
| 
						 | 
					541e6a4612 | ||
| 
						 | 
					43589a88c3 | ||
| 
						 | 
					15a6e75e7d | ||
| 
						 | 
					2681a37fad | ||
| d086c4c813 | |||
| f49fe21da5 | |||
| 717c898d48 | |||
| 085411132d | |||
| c8a6a9d81e | |||
| a0389db9ef | |||
| e6e4db7410 | |||
| 78542a533a | |||
| 46665dce88 | |||
| 4b420f313c | |||
| 42b4d0758f | |||
| 901c2a948f | |||
| 6fe5a29ebd | |||
| ce7223597c | 
							
								
								
									
										16
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -16,7 +16,7 @@ steps:
 | 
				
			|||||||
      - assets/node_modules/
 | 
					      - assets/node_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: test
 | 
					- name: test
 | 
				
			||||||
  image: elixir:1.13.4-alpine
 | 
					  image: elixir:1.14.1-alpine
 | 
				
			||||||
  environment:
 | 
					  environment:
 | 
				
			||||||
    TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
 | 
					    TEST_DATABASE_URL: ecto://postgres:postgres@database/cannery_test
 | 
				
			||||||
    HOST: testing.example.tld
 | 
					    HOST: testing.example.tld
 | 
				
			||||||
@@ -29,12 +29,16 @@ steps:
 | 
				
			|||||||
  - npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
 | 
					  - npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
 | 
				
			||||||
  - npm run --prefix ./assets deploy
 | 
					  - npm run --prefix ./assets deploy
 | 
				
			||||||
  - mix do phx.digest, gettext.extract
 | 
					  - mix do phx.digest, gettext.extract
 | 
				
			||||||
  - mix test
 | 
					  - mix test.all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: build and publish stable
 | 
					- name: build and publish stable
 | 
				
			||||||
  image: plugins/docker
 | 
					  image: thegeeklab/drone-docker-buildx
 | 
				
			||||||
 | 
					  privileged: true
 | 
				
			||||||
  settings:
 | 
					  settings:
 | 
				
			||||||
    repo: shibaobun/cannery
 | 
					    repo: shibaobun/cannery
 | 
				
			||||||
 | 
					    purge: true
 | 
				
			||||||
 | 
					    compress: true
 | 
				
			||||||
 | 
					    platforms: linux/amd64,linux/arm/v7
 | 
				
			||||||
    username:
 | 
					    username:
 | 
				
			||||||
      from_secret: docker_username
 | 
					      from_secret: docker_username
 | 
				
			||||||
    password:
 | 
					    password:
 | 
				
			||||||
@@ -45,9 +49,13 @@ steps:
 | 
				
			|||||||
    - stable
 | 
					    - stable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: build and publish tagged version
 | 
					- name: build and publish tagged version
 | 
				
			||||||
  image: plugins/docker
 | 
					  image: thegeeklab/drone-docker-buildx
 | 
				
			||||||
 | 
					  privileged: true
 | 
				
			||||||
  settings:
 | 
					  settings:
 | 
				
			||||||
    repo: shibaobun/cannery
 | 
					    repo: shibaobun/cannery
 | 
				
			||||||
 | 
					    purge: true
 | 
				
			||||||
 | 
					    compress: true
 | 
				
			||||||
 | 
					    platforms: linux/amd64,linux/arm/v7
 | 
				
			||||||
    username:
 | 
					    username:
 | 
				
			||||||
      from_secret: docker_username
 | 
					      from_secret: docker_username
 | 
				
			||||||
    password:
 | 
					    password:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
elixir 1.13.4-otp-24
 | 
					elixir 1.14.1-otp-25
 | 
				
			||||||
erlang 24.2
 | 
					erlang 25.1.2
 | 
				
			||||||
nodejs 16.13.2
 | 
					nodejs 18.12.1
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										82
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,85 @@
 | 
				
			|||||||
 | 
					# v0.8.1
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					- Show topbar on form submit/page refresh
 | 
				
			||||||
 | 
					- Make loading/reconnection less intrusive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.8.0
 | 
				
			||||||
 | 
					- Add search to catalog, ammo, container, tag and range index pages
 | 
				
			||||||
 | 
					- Tweak urls for catalog, ammo, containers, tags and shot records
 | 
				
			||||||
 | 
					- Fix bug with shot group chart not drawing lines between days correctly
 | 
				
			||||||
 | 
					- Improve cards across app (make them line up with each other)
 | 
				
			||||||
 | 
					- Update translations and add spanish!!! (thank you Brea and Hannah!)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.7.2
 | 
				
			||||||
 | 
					- Code improvements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.7.1
 | 
				
			||||||
 | 
					- Fix table component alignment and styling
 | 
				
			||||||
 | 
					- Fix toggle button styling
 | 
				
			||||||
 | 
					- Miscellanous code improvements
 | 
				
			||||||
 | 
					- Improve container index table
 | 
				
			||||||
 | 
					- Fix bug with ammo not updating after deleting shot group
 | 
				
			||||||
 | 
					- Replace ammo "added on" with "purchased on"
 | 
				
			||||||
 | 
					- Miscellaneous wording improvements
 | 
				
			||||||
 | 
					- Update translations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.7.0
 | 
				
			||||||
 | 
					- Add shading to table component
 | 
				
			||||||
 | 
					- Fix chart to sum by day
 | 
				
			||||||
 | 
					- Fix whitespace when copying invite url
 | 
				
			||||||
 | 
					- Make ammo type show page also display ammo groups as table
 | 
				
			||||||
 | 
					- Make container show page also display ammo groups as table
 | 
				
			||||||
 | 
					- Display CPR for ammo packs
 | 
				
			||||||
 | 
					- Add original count for ammo packs
 | 
				
			||||||
 | 
					- Add ammo pack CPR and original count to json export
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.6.0
 | 
				
			||||||
 | 
					- Update translations
 | 
				
			||||||
 | 
					- Display used-up date on used-up ammo
 | 
				
			||||||
 | 
					- Make ammo index page a bit more compact
 | 
				
			||||||
 | 
					- Make ammo index page filter used-up ammo
 | 
				
			||||||
 | 
					- Make ammo catalog page include ammo count
 | 
				
			||||||
 | 
					- Make ammo type show page a bit more compact
 | 
				
			||||||
 | 
					- Make ammo type show page include container names for each ammo
 | 
				
			||||||
 | 
					- Make ammo type show page filter used-up ammo
 | 
				
			||||||
 | 
					- Make container index page optionally display a table
 | 
				
			||||||
 | 
					- Make container show page a bit more compact
 | 
				
			||||||
 | 
					- Make container show page filter used-up ammo
 | 
				
			||||||
 | 
					- Forgot to add the logo as the favicon whoops
 | 
				
			||||||
 | 
					- Add graph to range page
 | 
				
			||||||
 | 
					- Add JSON export of data
 | 
				
			||||||
 | 
					- Add ammo cloning
 | 
				
			||||||
 | 
					- Add ammo type cloning
 | 
				
			||||||
 | 
					- Add container cloning
 | 
				
			||||||
 | 
					- Fix bug with moving ammo packs between containers
 | 
				
			||||||
 | 
					- Add button to set rounds left to 0 when creating a shot group
 | 
				
			||||||
 | 
					- Update project dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.5.4
 | 
				
			||||||
 | 
					- Rename "Ammo" tab to "Catalog", and "Manage" tab is now "Ammo"
 | 
				
			||||||
 | 
					- Ammo groups are now just referred to as Ammo or "Packs"
 | 
				
			||||||
 | 
					- URL paths now reflect new names
 | 
				
			||||||
 | 
					- Add pack and round count to container information
 | 
				
			||||||
 | 
					- Add cute logo >:3 Thank you [kalli](https://twitter.com/t0kkuro)!
 | 
				
			||||||
 | 
					- Add note about deleting an ammo type deleting all ammo of that type as well
 | 
				
			||||||
 | 
					- Prompt to create first ammo type before trying to create first ammo
 | 
				
			||||||
 | 
					- Add note about creating unlimited invites
 | 
				
			||||||
 | 
					- Update screenshot lol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.5.3
 | 
				
			||||||
 | 
					- Update French translation: Thank you [duponin](https://udongein.xyz/users/duponin)!
 | 
				
			||||||
 | 
					- Update German translation: Thank you [Kaia](https://shitposter.club/users/kaia)!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.5.2
 | 
				
			||||||
 | 
					- Add "Added on" date to ammo groups
 | 
				
			||||||
 | 
					- Add "Added on" date to ammo types
 | 
				
			||||||
 | 
					- Add "Registered on" date to user information
 | 
				
			||||||
 | 
					- Add language in user settings. The `LOCALE` environment variable will continue
 | 
				
			||||||
 | 
					  to set the default locale for the application.
 | 
				
			||||||
 | 
					- Add involvement links to home page
 | 
				
			||||||
 | 
					- Fix button text-wrapping
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# v0.5.1
 | 
					# v0.5.1
 | 
				
			||||||
- Add French translation: Thank you [duponin](https://udongein.xyz/users/duponin)!
 | 
					- Add French translation: Thank you [duponin](https://udongein.xyz/users/duponin)!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,8 +17,8 @@ If you're multilingual, this project can use your translations! Visit
 | 
				
			|||||||
  functions as short as possible while keeping variable names descriptive! For
 | 
					  functions as short as possible while keeping variable names descriptive! For
 | 
				
			||||||
  instance, use inline `do:` blocks for short functions and make your aliases as
 | 
					  instance, use inline `do:` blocks for short functions and make your aliases as
 | 
				
			||||||
  short as possible without introducing ambiguity.
 | 
					  short as possible without introducing ambiguity.
 | 
				
			||||||
  - I.e. since there's only one `Changeset` in the app, please alias
 | 
					  - I.e. since there's only one `AmmoGroup` in the app, please alias
 | 
				
			||||||
    `Changeset.t(Type.t())` instead of using `Ecto.Changeset.t(Long.Type.t())`
 | 
					    `AmmoGroup.t()` instead of using `Cannery.Ammo.AmmoGroup.t()`
 | 
				
			||||||
- Use pipelines when possible. If only calling a single method, a pipeline isn't
 | 
					- Use pipelines when possible. If only calling a single method, a pipeline isn't
 | 
				
			||||||
  strictly necessary but still encouraged for future modification.
 | 
					  strictly necessary but still encouraged for future modification.
 | 
				
			||||||
- Please add typespecs to your functions! Even your private functions may be
 | 
					- Please add typespecs to your functions! Even your private functions may be
 | 
				
			||||||
@@ -63,8 +63,7 @@ And as always, thank you!
 | 
				
			|||||||
  [`phx_gen_auth`](https://hexdocs.pm/phx_gen_auth/).
 | 
					  [`phx_gen_auth`](https://hexdocs.pm/phx_gen_auth/).
 | 
				
			||||||
- `Dockerfile` and example `docker-compose.yml`
 | 
					- `Dockerfile` and example `docker-compose.yml`
 | 
				
			||||||
- Automatic migrations in `MIX_ENV=prod` or Docker image
 | 
					- Automatic migrations in `MIX_ENV=prod` or Docker image
 | 
				
			||||||
- JS linting with [standard.js](https://standardjs.com), HEEx linting with
 | 
					- JS linting with [standard.js](https://standardjs.com)
 | 
				
			||||||
  [heex_formatter](https://github.com/feliperenan/heex_formatter)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Docs
 | 
					## Docs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -109,7 +108,7 @@ In `dev` mode, Cannery will listen for these environment variables at runtime.
 | 
				
			|||||||
  Defaults to `false`.
 | 
					  Defaults to `false`.
 | 
				
			||||||
- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`.
 | 
					- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`.
 | 
				
			||||||
- `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`.
 | 
					- `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`.
 | 
				
			||||||
- `LOCALE`: Sets a custom locale. Defaults to `en_US`.
 | 
					- `LOCALE`: Sets a custom default locale. Defaults to `en_US`.
 | 
				
			||||||
  - Available options: `en_US`, `de`, and `fr`
 | 
					  - Available options: `en_US`, `de`, and `fr`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## `MIX_ENV=test`
 | 
					## `MIX_ENV=test`
 | 
				
			||||||
@@ -144,3 +143,5 @@ Thank you so much for your contributions!
 | 
				
			|||||||
- shibao (https://misskey.bubbletea.dev/@shibao)
 | 
					- shibao (https://misskey.bubbletea.dev/@shibao)
 | 
				
			||||||
- kaia (https://shitposter.club/users/kaia)
 | 
					- kaia (https://shitposter.club/users/kaia)
 | 
				
			||||||
- duponin (https://udongein.xyz/users/duponin)
 | 
					- duponin (https://udongein.xyz/users/duponin)
 | 
				
			||||||
 | 
					- kalli (https://twitter.com/t0kkuro)
 | 
				
			||||||
 | 
					- brea (https://refusal.biz/users/tarperfume)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
FROM elixir:1.13.4-alpine AS build
 | 
					FROM elixir:1.14.1-alpine AS build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# install build dependencies
 | 
					# install build dependencies
 | 
				
			||||||
RUN apk add --no-cache build-base npm git python3
 | 
					RUN apk add --no-cache build-base npm git python3
 | 
				
			||||||
@@ -37,7 +37,7 @@ RUN mix do compile, release
 | 
				
			|||||||
FROM alpine:latest AS app
 | 
					FROM alpine:latest AS app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apk upgrade --no-cache && \
 | 
					RUN apk upgrade --no-cache && \
 | 
				
			||||||
    apk add --no-cache bash openssl libgcc libstdc++ ncurses-libs
 | 
					    apk add --no-cache bash openssl libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
# Cannery
 | 
					# Cannery
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The self-hosted firearm tracker website.
 | 
					The self-hosted firearm tracker website.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,7 +63,7 @@ You can use the following environment variables to configure Cannery in
 | 
				
			|||||||
  with `docker run -it shibaobun/cannery mix phx.gen.secret` and set for server to start.
 | 
					  with `docker run -it shibaobun/cannery mix phx.gen.secret` and set for server to start.
 | 
				
			||||||
- `REGISTRATION`: Controls if user sign-up should be invite only or set to
 | 
					- `REGISTRATION`: Controls if user sign-up should be invite only or set to
 | 
				
			||||||
  public. Set to `public` to enable public registration. Defaults to `invite`.
 | 
					  public. Set to `public` to enable public registration. Defaults to `invite`.
 | 
				
			||||||
- `LOCALE`: Sets a custom locale. Defaults to `en_US`
 | 
					- `LOCALE`: Sets a custom default locale. Defaults to `en_US`
 | 
				
			||||||
  - Available options: `en_US`, `de`, and `fr`
 | 
					  - Available options: `en_US`, `de`, and `fr`
 | 
				
			||||||
- `SMTP_HOST`: The url for your SMTP email provider. Must be set
 | 
					- `SMTP_HOST`: The url for your SMTP email provider. Must be set
 | 
				
			||||||
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
 | 
					- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,12 +25,13 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
 | 
				
			|||||||
  100% { scale: 1.0; opacity: 1; }
 | 
					  100% { scale: 1.0; opacity: 1; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.phx-connected > #disconnect, #loading {
 | 
					// disconnect toast
 | 
				
			||||||
 | 
					.phx-connected > #disconnect {
 | 
				
			||||||
  opacity: 0 !important;
 | 
					  opacity: 0 !important;
 | 
				
			||||||
  pointer-events: none;
 | 
					  pointer-events: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.phx-loading:not(.phx-error) > #loading, .phx-error > #disconnect {
 | 
					.phx-error > #disconnect {
 | 
				
			||||||
  opacity: 0.95 !important;
 | 
					  opacity: 0.95 !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .btn {
 | 
					  .btn {
 | 
				
			||||||
    @apply inline-block break-all min-w-4;
 | 
					    @apply inline-block break-words;
 | 
				
			||||||
    @apply focus:outline-none px-4 py-2 rounded-lg;
 | 
					    @apply focus:outline-none px-4 py-2 rounded-lg;
 | 
				
			||||||
    @apply shadow-sm focus:shadow-lg;
 | 
					    @apply shadow-sm focus:shadow-lg;
 | 
				
			||||||
    @apply transition-all duration-300 ease-in-out;
 | 
					    @apply transition-all duration-300 ease-in-out;
 | 
				
			||||||
@@ -52,7 +52,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .link {
 | 
					  .link {
 | 
				
			||||||
    @apply inline-block break-all min-w-4;
 | 
					    @apply inline-block break-words;
 | 
				
			||||||
    @apply hover:underline;
 | 
					    @apply hover:underline;
 | 
				
			||||||
    @apply transition-colors duration-500 ease-in-out;
 | 
					    @apply transition-colors duration-500 ease-in-out;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,7 @@ import { Socket } from 'phoenix'
 | 
				
			|||||||
import { LiveSocket } from 'phoenix_live_view'
 | 
					import { LiveSocket } from 'phoenix_live_view'
 | 
				
			||||||
import topbar from '../vendor/topbar'
 | 
					import topbar from '../vendor/topbar'
 | 
				
			||||||
import MaintainAttrs from './maintain_attrs'
 | 
					import MaintainAttrs from './maintain_attrs'
 | 
				
			||||||
 | 
					import ShotLogChart from './shot_log_chart'
 | 
				
			||||||
import Alpine from 'alpinejs'
 | 
					import Alpine from 'alpinejs'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
 | 
					const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
 | 
				
			||||||
@@ -36,7 +37,7 @@ const liveSocket = new LiveSocket('/live', Socket, {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  params: { _csrf_token: csrfToken },
 | 
					  params: { _csrf_token: csrfToken },
 | 
				
			||||||
  hooks: { MaintainAttrs }
 | 
					  hooks: { MaintainAttrs, ShotLogChart }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// alpine.js
 | 
					// alpine.js
 | 
				
			||||||
@@ -47,6 +48,8 @@ Alpine.start()
 | 
				
			|||||||
topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' })
 | 
					topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' })
 | 
				
			||||||
window.addEventListener('phx:page-loading-start', info => topbar.show())
 | 
					window.addEventListener('phx:page-loading-start', info => topbar.show())
 | 
				
			||||||
window.addEventListener('phx:page-loading-stop', info => topbar.hide())
 | 
					window.addEventListener('phx:page-loading-stop', info => topbar.hide())
 | 
				
			||||||
 | 
					window.addEventListener('submit', info => topbar.show())
 | 
				
			||||||
 | 
					window.addEventListener('beforeunload', info => topbar.show())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// connect if there are any LiveViews on the page
 | 
					// connect if there are any LiveViews on the page
 | 
				
			||||||
liveSocket.connect()
 | 
					liveSocket.connect()
 | 
				
			||||||
@@ -66,3 +69,8 @@ window.addEventListener('cannery:clipcopy', (event) => {
 | 
				
			|||||||
    window.alert('Sorry, your browser does not support clipboard copy.')
 | 
					    window.alert('Sorry, your browser does not support clipboard copy.')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set input value to 0
 | 
				
			||||||
 | 
					window.addEventListener('cannery:set-zero', (event) => {
 | 
				
			||||||
 | 
					  event.target.value = 0
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								assets/js/shot_log_chart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								assets/js/shot_log_chart.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					import { Chart, Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale } from 'chart.js'
 | 
				
			||||||
 | 
					import 'chartjs-adapter-date-fns'
 | 
				
			||||||
 | 
					Chart.register(Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  initalizeChart (el) {
 | 
				
			||||||
 | 
					    const data = JSON.parse(el.dataset.chartData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.el.chart = new Chart(el, {
 | 
				
			||||||
 | 
					      type: 'line',
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        datasets: [{
 | 
				
			||||||
 | 
					          label: el.dataset.label,
 | 
				
			||||||
 | 
					          data: data.map(({ date, count, label }) => ({
 | 
				
			||||||
 | 
					            label,
 | 
				
			||||||
 | 
					            x: date,
 | 
				
			||||||
 | 
					            y: count
 | 
				
			||||||
 | 
					          })),
 | 
				
			||||||
 | 
					          backgroundColor: `${el.dataset.color}77`,
 | 
				
			||||||
 | 
					          borderColor: el.dataset.color,
 | 
				
			||||||
 | 
					          fill: true,
 | 
				
			||||||
 | 
					          borderWidth: 3,
 | 
				
			||||||
 | 
					          pointBorderWidth: 1
 | 
				
			||||||
 | 
					        }]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      options: {
 | 
				
			||||||
 | 
					        elements: {
 | 
				
			||||||
 | 
					          point: {
 | 
				
			||||||
 | 
					            radius: 9,
 | 
				
			||||||
 | 
					            hoverRadius: 12
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        plugins: {
 | 
				
			||||||
 | 
					          legend: {
 | 
				
			||||||
 | 
					            position: 'bottom',
 | 
				
			||||||
 | 
					            labels: {
 | 
				
			||||||
 | 
					              padding: 20
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          tooltip: {
 | 
				
			||||||
 | 
					            displayColors: false,
 | 
				
			||||||
 | 
					            callbacks: {
 | 
				
			||||||
 | 
					              title: (contexts) => contexts.map(({ raw: { x } }) => Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' }).format(new Date(x))),
 | 
				
			||||||
 | 
					              label: ({ raw: { label } }) => label
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        scales: {
 | 
				
			||||||
 | 
					          y: {
 | 
				
			||||||
 | 
					            beginAtZero: true,
 | 
				
			||||||
 | 
					            stacked: true,
 | 
				
			||||||
 | 
					            grace: '15%',
 | 
				
			||||||
 | 
					            ticks: {
 | 
				
			||||||
 | 
					              padding: 15
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          x: {
 | 
				
			||||||
 | 
					            type: 'time',
 | 
				
			||||||
 | 
					            time: {
 | 
				
			||||||
 | 
					              unit: 'day'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        transitions: {
 | 
				
			||||||
 | 
					          show: {
 | 
				
			||||||
 | 
					            animations: {
 | 
				
			||||||
 | 
					              x: {
 | 
				
			||||||
 | 
					                from: 0
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          hide: {
 | 
				
			||||||
 | 
					            animations: {
 | 
				
			||||||
 | 
					              x: {
 | 
				
			||||||
 | 
					                to: 0
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  updateChart (el) {
 | 
				
			||||||
 | 
					    const data = JSON.parse(el.dataset.chartData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.el.chart.data = {
 | 
				
			||||||
 | 
					      datasets: [{
 | 
				
			||||||
 | 
					        label: el.dataset.label,
 | 
				
			||||||
 | 
					        data: data.map(({ date, count, label }) => ({
 | 
				
			||||||
 | 
					          label,
 | 
				
			||||||
 | 
					          x: date,
 | 
				
			||||||
 | 
					          y: count
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					        backgroundColor: `${el.dataset.color}77`,
 | 
				
			||||||
 | 
					        borderColor: el.dataset.color,
 | 
				
			||||||
 | 
					        fill: true,
 | 
				
			||||||
 | 
					        borderWidth: 3,
 | 
				
			||||||
 | 
					        pointBorderWidth: 1
 | 
				
			||||||
 | 
					      }]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.el.chart.update()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted () { this.initalizeChart(this.el) },
 | 
				
			||||||
 | 
					  updated () { this.updateChart(this.el) }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15632
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15632
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,6 +2,10 @@
 | 
				
			|||||||
  "repository": {},
 | 
					  "repository": {},
 | 
				
			||||||
  "description": " ",
 | 
					  "description": " ",
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "engines": {
 | 
				
			||||||
 | 
					    "node": "18.12.1",
 | 
				
			||||||
 | 
					    "npm": "8.19.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "deploy": "NODE_ENV=production webpack --mode production",
 | 
					    "deploy": "NODE_ENV=production webpack --mode production",
 | 
				
			||||||
    "watch": "webpack --mode development --watch --watch-options-stdin",
 | 
					    "watch": "webpack --mode development --watch --watch-options-stdin",
 | 
				
			||||||
@@ -9,36 +13,37 @@
 | 
				
			|||||||
    "test": "standard"
 | 
					    "test": "standard"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@fortawesome/fontawesome-free": "^5.15.4",
 | 
					    "@fortawesome/fontawesome-free": "^6.1.1",
 | 
				
			||||||
    "alpinejs": "^3.9.0",
 | 
					    "alpinejs": "^3.10.2",
 | 
				
			||||||
 | 
					    "chart.js": "^3.9.1",
 | 
				
			||||||
 | 
					    "chartjs-adapter-date-fns": "^2.0.0",
 | 
				
			||||||
 | 
					    "date-fns": "^2.29.3",
 | 
				
			||||||
    "phoenix": "file:../deps/phoenix",
 | 
					    "phoenix": "file:../deps/phoenix",
 | 
				
			||||||
    "phoenix_html": "file:../deps/phoenix_html",
 | 
					    "phoenix_html": "file:../deps/phoenix_html",
 | 
				
			||||||
    "phoenix_live_view": "file:../deps/phoenix_live_view",
 | 
					    "phoenix_live_view": "file:../deps/phoenix_live_view",
 | 
				
			||||||
    "topbar": "^0.1.4"
 | 
					    "topbar": "^1.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/core": "^7.15.0",
 | 
					    "@babel/core": "^7.17.10",
 | 
				
			||||||
    "@babel/preset-env": "^7.15.0",
 | 
					    "@babel/preset-env": "^7.17.10",
 | 
				
			||||||
    "autoprefixer": "^10.2.6",
 | 
					    "autoprefixer": "^10.4.7",
 | 
				
			||||||
    "babel-loader": "^8.2.2",
 | 
					    "babel-loader": "^8.2.5",
 | 
				
			||||||
    "copy-webpack-plugin": "^9.0.0",
 | 
					    "copy-webpack-plugin": "^10.2.4",
 | 
				
			||||||
    "css-loader": "^5.2.7",
 | 
					    "css-loader": "^6.7.1",
 | 
				
			||||||
    "css-minimizer-webpack-plugin": "^3.0.1",
 | 
					    "css-minimizer-webpack-plugin": "^3.4.1",
 | 
				
			||||||
    "file-loader": "^6.2.0",
 | 
					    "file-loader": "^6.2.0",
 | 
				
			||||||
    "hard-source-webpack-plugin": "^0.13.1",
 | 
					    "mini-css-extract-plugin": "^2.6.0",
 | 
				
			||||||
    "mini-css-extract-plugin": "^1.6.0",
 | 
					    "postcss": "^8.4.13",
 | 
				
			||||||
    "node-sass": "^7.0.1",
 | 
					    "postcss-import": "^14.1.0",
 | 
				
			||||||
    "postcss": "^8.3.6",
 | 
					    "postcss-loader": "^6.2.1",
 | 
				
			||||||
    "postcss-import": "^14.0.2",
 | 
					    "postcss-preset-env": "^7.5.0",
 | 
				
			||||||
    "postcss-loader": "^6.1.1",
 | 
					    "sass": "^1.56.0",
 | 
				
			||||||
    "postcss-preset-env": "^7.3.1",
 | 
					    "sass-loader": "^12.6.0",
 | 
				
			||||||
    "sass-loader": "^12.1.0",
 | 
					    "standard": "^17.0.0",
 | 
				
			||||||
    "standard": "^16.0.4",
 | 
					    "tailwindcss": "^3.0.24",
 | 
				
			||||||
    "style-loader": "^3.2.1",
 | 
					    "terser-webpack-plugin": "^5.3.1",
 | 
				
			||||||
    "tailwindcss": "^2.2.7",
 | 
					    "webpack": "^5.72.0",
 | 
				
			||||||
    "terser-webpack-plugin": "^5.1.3",
 | 
					    "webpack-cli": "^4.9.2",
 | 
				
			||||||
    "webpack": "^5.67.0",
 | 
					    "webpack-dev-server": "^4.9.0"
 | 
				
			||||||
    "webpack-cli": "^4.8.0",
 | 
					 | 
				
			||||||
    "webpack-dev-server": "^4.7.4"
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								assets/static/images/cannery.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/static/images/cannery.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 84 KiB  | 
							
								
								
									
										38
									
								
								assets/static/images/cannery.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								assets/static/images/cannery.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 516 KiB  | 
@@ -1,24 +1,18 @@
 | 
				
			|||||||
const colors = require('tailwindcss/colors')
 | 
					const colors = require('tailwindcss/colors')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  purge: [
 | 
					  content: [
 | 
				
			||||||
    '../lib/**/*.ex',
 | 
					    '../lib/**/*.{ex,heex,leex,eex}',
 | 
				
			||||||
    '../lib/**/*.heex',
 | 
					 | 
				
			||||||
    '../lib/**/*.leex',
 | 
					 | 
				
			||||||
    '../lib/**/*.eex',
 | 
					 | 
				
			||||||
    './js/**/*.js'
 | 
					    './js/**/*.js'
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  darkMode: 'media',
 | 
					 | 
				
			||||||
  theme: {
 | 
					  theme: {
 | 
				
			||||||
    colors: {
 | 
					    colors: {
 | 
				
			||||||
      transparent: 'transparent',
 | 
					      transparent: 'transparent',
 | 
				
			||||||
      current: 'currentColor',
 | 
					      current: 'currentColor',
 | 
				
			||||||
 | 
					 | 
				
			||||||
      primary: colors.gray,
 | 
					      primary: colors.gray,
 | 
				
			||||||
 | 
					 | 
				
			||||||
      black: colors.black,
 | 
					      black: colors.black,
 | 
				
			||||||
      white: colors.white,
 | 
					      white: colors.white,
 | 
				
			||||||
      gray: colors.trueGray,
 | 
					      gray: colors.neutral,
 | 
				
			||||||
      indigo: colors.indigo,
 | 
					      indigo: colors.indigo,
 | 
				
			||||||
      red: colors.rose,
 | 
					      red: colors.rose,
 | 
				
			||||||
      yellow: colors.amber
 | 
					      yellow: colors.amber
 | 
				
			||||||
@@ -45,11 +39,5 @@ module.exports = {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  variants: {
 | 
					 | 
				
			||||||
    extend: {
 | 
					 | 
				
			||||||
      backgroundColor: ['active'],
 | 
					 | 
				
			||||||
      borderColor: ['active']
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  plugins: []
 | 
					  plugins: []
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,19 +44,14 @@ module.exports = (env, options) => {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
 | 
					          test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
 | 
				
			||||||
          loader: 'file-loader',
 | 
					          type: 'asset/resource',
 | 
				
			||||||
          options: {
 | 
					          generator: { filename: 'fonts/[name][ext]' }
 | 
				
			||||||
            name: '[name].[ext]?[hash]',
 | 
					 | 
				
			||||||
            outputPath: '../fonts'
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    plugins: [
 | 
					    plugins: [
 | 
				
			||||||
      new MiniCssExtractPlugin({ filename: '../css/app.css' }),
 | 
					      new MiniCssExtractPlugin({ filename: '../css/app.css' }),
 | 
				
			||||||
      new CopyWebpackPlugin({
 | 
					      new CopyWebpackPlugin({ patterns: [{ from: 'static/', to: '../' }] })
 | 
				
			||||||
        patterns: [{ from: 'static/', to: '../' }]
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								home.png
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								home.png
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 113 KiB  | 
@@ -23,7 +23,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      nil
 | 
					      nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user_by_email(String.t()) :: User.t() | nil
 | 
					  @spec get_user_by_email(email :: String.t()) :: User.t() | nil
 | 
				
			||||||
  def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
 | 
					  def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -38,7 +38,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      nil
 | 
					      nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user_by_email_and_password(String.t(), String.t()) ::
 | 
					  @spec get_user_by_email_and_password(email :: String.t(), password :: String.t()) ::
 | 
				
			||||||
          User.t() | nil
 | 
					          User.t() | nil
 | 
				
			||||||
  def get_user_by_email_and_password(email, password)
 | 
					  def get_user_by_email_and_password(email, password)
 | 
				
			||||||
      when is_binary(email) and is_binary(password) do
 | 
					      when is_binary(email) and is_binary(password) do
 | 
				
			||||||
@@ -86,7 +86,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      [%User{}]
 | 
					      [%User{}]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_users_by_role(:admin | :user) :: [User.t()]
 | 
					  @spec list_users_by_role(User.role()) :: [User.t()]
 | 
				
			||||||
  def list_users_by_role(role) do
 | 
					  def list_users_by_role(role) do
 | 
				
			||||||
    role = role |> to_string()
 | 
					    role = role |> to_string()
 | 
				
			||||||
    Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
 | 
					    Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
 | 
				
			||||||
@@ -106,15 +106,21 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec register_user(map()) :: {:ok, User.t()} | {:error, Changeset.t(User.new_user())}
 | 
					  @spec register_user(attrs :: map()) :: {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def register_user(attrs) do
 | 
					  def register_user(attrs) do
 | 
				
			||||||
    # if no registered users, make first user an admin
 | 
					    Multi.new()
 | 
				
			||||||
    role =
 | 
					    |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
 | 
				
			||||||
      if Repo.one!(from u in User, select: count(u.id), distinct: true) == 0,
 | 
					    |> Multi.insert(:add_user, fn %{users_count: count} ->
 | 
				
			||||||
        do: "admin",
 | 
					      # if no registered users, make first user an admin
 | 
				
			||||||
        else: "user"
 | 
					      role = if count == 0, do: "admin", else: "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    %User{} |> User.registration_changeset(attrs |> Map.put("role", role)) |> Repo.insert()
 | 
					      User.registration_changeset(attrs) |> User.role_changeset(role)
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Repo.transaction()
 | 
				
			||||||
 | 
					    |> case do
 | 
				
			||||||
 | 
					      {:ok, %{add_user: user}} -> {:ok, user}
 | 
				
			||||||
 | 
					      {:error, :add_user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -126,12 +132,10 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{data: %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_registration(User.t() | User.new_user()) ::
 | 
					  @spec change_user_registration() :: User.changeset()
 | 
				
			||||||
          Changeset.t(User.t() | User.new_user())
 | 
					  @spec change_user_registration(attrs :: map()) :: User.changeset()
 | 
				
			||||||
  @spec change_user_registration(User.t() | User.new_user(), map()) ::
 | 
					  def change_user_registration(attrs \\ %{}),
 | 
				
			||||||
          Changeset.t(User.t() | User.new_user())
 | 
					    do: User.registration_changeset(attrs, hash_password: false)
 | 
				
			||||||
  def change_user_registration(user, attrs \\ %{}),
 | 
					 | 
				
			||||||
    do: User.registration_changeset(user, attrs, hash_password: false)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Settings
 | 
					  ## Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,7 +148,8 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{data: %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_email(User.t(), map()) :: Changeset.t(User.t())
 | 
					  @spec change_user_email(User.t()) :: User.changeset()
 | 
				
			||||||
 | 
					  @spec change_user_email(User.t(), attrs :: map()) :: User.changeset()
 | 
				
			||||||
  def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
 | 
					  def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -156,7 +161,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{data: %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_role(User.t(), atom()) :: Changeset.t(User.t())
 | 
					  @spec change_user_role(User.t(), User.role()) :: User.changeset()
 | 
				
			||||||
  def change_user_role(user, role), do: User.role_changeset(user, role)
 | 
					  def change_user_role(user, role), do: User.role_changeset(user, role)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -172,8 +177,8 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec apply_user_email(User.t(), String.t(), map()) ::
 | 
					  @spec apply_user_email(User.t(), email :: String.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def apply_user_email(user, password, attrs) do
 | 
					  def apply_user_email(user, password, attrs) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> User.email_changeset(attrs)
 | 
					    |> User.email_changeset(attrs)
 | 
				
			||||||
@@ -187,7 +192,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
  If the token matches, the user email is updated and the token is deleted.
 | 
					  If the token matches, the user email is updated and the token is deleted.
 | 
				
			||||||
  The confirmed_at date is also updated to the current time.
 | 
					  The confirmed_at date is also updated to the current time.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_user_email(User.t(), String.t()) :: :ok | :error
 | 
					  @spec update_user_email(User.t(), token :: String.t()) :: :ok | :error
 | 
				
			||||||
  def update_user_email(user, token) do
 | 
					  def update_user_email(user, token) do
 | 
				
			||||||
    context = "change:#{user.email}"
 | 
					    context = "change:#{user.email}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -200,7 +205,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec user_email_multi(User.t(), String.t(), String.t()) :: Multi.t()
 | 
					  @spec user_email_multi(User.t(), email :: String.t(), context :: String.t()) :: Multi.t()
 | 
				
			||||||
  defp user_email_multi(user, email, context) do
 | 
					  defp user_email_multi(user, email, context) do
 | 
				
			||||||
    changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
 | 
					    changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -218,7 +223,8 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      {:ok, %{to: ..., body: ...}}
 | 
					      {:ok, %{to: ..., body: ...}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec deliver_update_email_instructions(User.t(), String.t(), function) :: Job.t()
 | 
					  @spec deliver_update_email_instructions(User.t(), current_email :: String.t(), function) ::
 | 
				
			||||||
 | 
					          Job.t()
 | 
				
			||||||
  def deliver_update_email_instructions(user, current_email, update_email_url_fun)
 | 
					  def deliver_update_email_instructions(user, current_email, update_email_url_fun)
 | 
				
			||||||
      when is_function(update_email_url_fun, 1) do
 | 
					      when is_function(update_email_url_fun, 1) do
 | 
				
			||||||
    {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
 | 
					    {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
 | 
				
			||||||
@@ -235,7 +241,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{data: %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_password(User.t(), map()) :: Changeset.t(User.t())
 | 
					  @spec change_user_password(User.t(), attrs :: map()) :: User.changeset()
 | 
				
			||||||
  def change_user_password(user, attrs \\ %{}),
 | 
					  def change_user_password(user, attrs \\ %{}),
 | 
				
			||||||
    do: User.password_changeset(user, attrs, hash_password: false)
 | 
					    do: User.password_changeset(user, attrs, hash_password: false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -251,8 +257,8 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_user_password(User.t(), String.t(), map()) ::
 | 
					  @spec update_user_password(User.t(), String.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def update_user_password(user, password, attrs) do
 | 
					  def update_user_password(user, password, attrs) do
 | 
				
			||||||
    changeset =
 | 
					    changeset =
 | 
				
			||||||
      user
 | 
					      user
 | 
				
			||||||
@@ -269,6 +275,35 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns an `%Changeset{}` for changing the user locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> change_user_locale(user)
 | 
				
			||||||
 | 
					      %Changeset{data: %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec change_user_locale(User.t()) :: User.changeset()
 | 
				
			||||||
 | 
					  def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Updates the user locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> update_user_locale(user, "valid locale")
 | 
				
			||||||
 | 
					      {:ok, %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> update_user_password(user, "invalid locale")
 | 
				
			||||||
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec update_user_locale(User.t(), locale :: String.t()) ::
 | 
				
			||||||
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
 | 
					  def update_user_locale(user, locale),
 | 
				
			||||||
 | 
					    do: user |> User.locale_changeset(locale) |> Repo.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes a user. must be performed by an admin or the same user!
 | 
					  Deletes a user. must be performed by an admin or the same user!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -281,7 +316,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      %User{}
 | 
					      %User{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_user!(User.t(), User.t()) :: User.t()
 | 
					  @spec delete_user!(user_to_delete :: User.t(), User.t()) :: User.t()
 | 
				
			||||||
  def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
 | 
					  def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
 | 
				
			||||||
  def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
 | 
					  def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -300,7 +335,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets the user with the given signed token.
 | 
					  Gets the user with the given signed token.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user_by_session_token(String.t()) :: User.t()
 | 
					  @spec get_user_by_session_token(token :: String.t()) :: User.t()
 | 
				
			||||||
  def get_user_by_session_token(token) do
 | 
					  def get_user_by_session_token(token) do
 | 
				
			||||||
    {:ok, query} = UserToken.verify_session_token_query(token)
 | 
					    {:ok, query} = UserToken.verify_session_token_query(token)
 | 
				
			||||||
    Repo.one(query)
 | 
					    Repo.one(query)
 | 
				
			||||||
@@ -309,7 +344,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes the signed token with the given context.
 | 
					  Deletes the signed token with the given context.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_session_token(String.t()) :: :ok
 | 
					  @spec delete_session_token(token :: String.t()) :: :ok
 | 
				
			||||||
  def delete_session_token(token) do
 | 
					  def delete_session_token(token) do
 | 
				
			||||||
    Repo.delete_all(UserToken.token_and_context_query(token, "session"))
 | 
					    Repo.delete_all(UserToken.token_and_context_query(token, "session"))
 | 
				
			||||||
    :ok
 | 
					    :ok
 | 
				
			||||||
@@ -365,7 +400,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
  If the token matches, the user account is marked as confirmed
 | 
					  If the token matches, the user account is marked as confirmed
 | 
				
			||||||
  and the token is deleted.
 | 
					  and the token is deleted.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec confirm_user(String.t()) :: {:ok, User.t()} | atom()
 | 
					  @spec confirm_user(token :: String.t()) :: {:ok, User.t()} | :error
 | 
				
			||||||
  def confirm_user(token) do
 | 
					  def confirm_user(token) do
 | 
				
			||||||
    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
 | 
					    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
 | 
				
			||||||
         %User{} = user <- Repo.one(query),
 | 
					         %User{} = user <- Repo.one(query),
 | 
				
			||||||
@@ -414,7 +449,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      nil
 | 
					      nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user_by_reset_password_token(String.t()) :: User.t() | nil
 | 
					  @spec get_user_by_reset_password_token(token :: String.t()) :: User.t() | nil
 | 
				
			||||||
  def get_user_by_reset_password_token(token) do
 | 
					  def get_user_by_reset_password_token(token) do
 | 
				
			||||||
    with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
 | 
					    with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
 | 
				
			||||||
         %User{} = user <- Repo.one(query) do
 | 
					         %User{} = user <- Repo.one(query) do
 | 
				
			||||||
@@ -436,7 +471,8 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec reset_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
					  @spec reset_user_password(User.t(), attrs :: map()) ::
 | 
				
			||||||
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def reset_user_password(user, attrs) do
 | 
					  def reset_user_password(user, attrs) do
 | 
				
			||||||
    Multi.new()
 | 
					    Multi.new()
 | 
				
			||||||
    |> Multi.update(:user, User.password_changeset(user, attrs))
 | 
					    |> Multi.update(:user, User.password_changeset(user, attrs))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,14 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Cannery.{Accounts.User, Invites.Invite}
 | 
					  alias Cannery.{Accounts.User, Invites.Invite}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :email,
 | 
				
			||||||
 | 
					             :confirmed_at,
 | 
				
			||||||
 | 
					             :role,
 | 
				
			||||||
 | 
					             :locale
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @derive {Inspect, except: [:password]}
 | 
					  @derive {Inspect, except: [:password]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
@@ -18,6 +26,7 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
    field :hashed_password, :string
 | 
					    field :hashed_password, :string
 | 
				
			||||||
    field :confirmed_at, :naive_datetime
 | 
					    field :confirmed_at, :naive_datetime
 | 
				
			||||||
    field :role, Ecto.Enum, values: [:admin, :user], default: :user
 | 
					    field :role, Ecto.Enum, values: [:admin, :user], default: :user
 | 
				
			||||||
 | 
					    field :locale, :string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    has_many :invites, Invite, on_delete: :delete_all
 | 
					    has_many :invites, Invite, on_delete: :delete_all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,13 +39,16 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
          password: String.t(),
 | 
					          password: String.t(),
 | 
				
			||||||
          hashed_password: String.t(),
 | 
					          hashed_password: String.t(),
 | 
				
			||||||
          confirmed_at: NaiveDateTime.t(),
 | 
					          confirmed_at: NaiveDateTime.t(),
 | 
				
			||||||
          role: atom(),
 | 
					          role: role(),
 | 
				
			||||||
 | 
					          locale: String.t() | nil,
 | 
				
			||||||
          invites: [Invite.t()],
 | 
					          invites: [Invite.t()],
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_user :: %User{}
 | 
					  @type new_user :: %User{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_user())
 | 
				
			||||||
 | 
					  @type role :: :admin | :user | String.t()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  A user changeset for registration.
 | 
					  A user changeset for registration.
 | 
				
			||||||
@@ -55,26 +67,24 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
      validations on a LiveView form), this option can be set to `false`.
 | 
					      validations on a LiveView form), this option can be set to `false`.
 | 
				
			||||||
      Defaults to `true`.
 | 
					      Defaults to `true`.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec registration_changeset(t() | new_user(), attrs :: map()) :: Changeset.t(t() | new_user())
 | 
					  @spec registration_changeset(attrs :: map()) :: changeset()
 | 
				
			||||||
  @spec registration_changeset(t() | new_user(), attrs :: map(), opts :: keyword()) ::
 | 
					  @spec registration_changeset(attrs :: map(), opts :: keyword()) :: changeset()
 | 
				
			||||||
          Changeset.t(t() | new_user())
 | 
					  def registration_changeset(attrs, opts \\ []) do
 | 
				
			||||||
  def registration_changeset(user, attrs, opts \\ []) do
 | 
					    %User{}
 | 
				
			||||||
    user
 | 
					    |> cast(attrs, [:email, :password, :locale])
 | 
				
			||||||
    |> cast(attrs, [:email, :password, :role])
 | 
					 | 
				
			||||||
    |> validate_email()
 | 
					    |> validate_email()
 | 
				
			||||||
    |> validate_password(opts)
 | 
					    |> validate_password(opts)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  A user changeset for role.
 | 
					  A user changeset for role.
 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec role_changeset(t(), role :: atom()) :: Changeset.t(t())
 | 
					  @spec role_changeset(t() | new_user() | changeset(), role()) :: changeset()
 | 
				
			||||||
  def role_changeset(user, role) do
 | 
					  def role_changeset(user, role) do
 | 
				
			||||||
    user |> cast(%{"role" => role}, [:role])
 | 
					    user |> cast(%{"role" => role}, [:role])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec validate_email(Changeset.t(t() | new_user())) :: Changeset.t(t() | new_user())
 | 
					  @spec validate_email(changeset()) :: changeset()
 | 
				
			||||||
  defp validate_email(changeset) do
 | 
					  defp validate_email(changeset) do
 | 
				
			||||||
    changeset
 | 
					    changeset
 | 
				
			||||||
    |> validate_required([:email])
 | 
					    |> validate_required([:email])
 | 
				
			||||||
@@ -86,8 +96,8 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
    |> unique_constraint(:email)
 | 
					    |> unique_constraint(:email)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec validate_password(Changeset.t(t() | new_user()), opts :: keyword()) ::
 | 
					  @spec validate_password(changeset(), opts :: keyword()) ::
 | 
				
			||||||
          Changeset.t(t() | new_user())
 | 
					          changeset()
 | 
				
			||||||
  defp validate_password(changeset, opts) do
 | 
					  defp validate_password(changeset, opts) do
 | 
				
			||||||
    changeset
 | 
					    changeset
 | 
				
			||||||
    |> validate_required([:password])
 | 
					    |> validate_required([:password])
 | 
				
			||||||
@@ -98,8 +108,7 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
    |> maybe_hash_password(opts)
 | 
					    |> maybe_hash_password(opts)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec maybe_hash_password(Changeset.t(t() | new_user()), opts :: keyword()) ::
 | 
					  @spec maybe_hash_password(changeset(), opts :: keyword()) :: changeset()
 | 
				
			||||||
          Changeset.t(t() | new_user())
 | 
					 | 
				
			||||||
  defp maybe_hash_password(changeset, opts) do
 | 
					  defp maybe_hash_password(changeset, opts) do
 | 
				
			||||||
    hash_password? = Keyword.get(opts, :hash_password, true)
 | 
					    hash_password? = Keyword.get(opts, :hash_password, true)
 | 
				
			||||||
    password = get_change(changeset, :password)
 | 
					    password = get_change(changeset, :password)
 | 
				
			||||||
@@ -118,7 +127,7 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  It requires the email to change otherwise an error is added.
 | 
					  It requires the email to change otherwise an error is added.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec email_changeset(t(), attrs :: map()) :: Changeset.t(t())
 | 
					  @spec email_changeset(t(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def email_changeset(user, attrs) do
 | 
					  def email_changeset(user, attrs) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> cast(attrs, [:email])
 | 
					    |> cast(attrs, [:email])
 | 
				
			||||||
@@ -141,8 +150,8 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
      validations on a LiveView form), this option can be set to `false`.
 | 
					      validations on a LiveView form), this option can be set to `false`.
 | 
				
			||||||
      Defaults to `true`.
 | 
					      Defaults to `true`.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec password_changeset(t(), attrs :: map()) :: Changeset.t(t())
 | 
					  @spec password_changeset(t(), attrs :: map()) :: changeset()
 | 
				
			||||||
  @spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: Changeset.t(t())
 | 
					  @spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: changeset()
 | 
				
			||||||
  def password_changeset(user, attrs, opts \\ []) do
 | 
					  def password_changeset(user, attrs, opts \\ []) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> cast(attrs, [:password])
 | 
					    |> cast(attrs, [:password])
 | 
				
			||||||
@@ -153,7 +162,7 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Confirms the account by setting `confirmed_at`.
 | 
					  Confirms the account by setting `confirmed_at`.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec confirm_changeset(t() | Changeset.t(t())) :: Changeset.t(t())
 | 
					  @spec confirm_changeset(t() | changeset()) :: changeset()
 | 
				
			||||||
  def confirm_changeset(user_or_changeset) do
 | 
					  def confirm_changeset(user_or_changeset) do
 | 
				
			||||||
    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
					    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
				
			||||||
    user_or_changeset |> change(confirmed_at: now)
 | 
					    user_or_changeset |> change(confirmed_at: now)
 | 
				
			||||||
@@ -179,10 +188,20 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Validates the current password otherwise adds an error to the changeset.
 | 
					  Validates the current password otherwise adds an error to the changeset.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec validate_current_password(Changeset.t(t()), String.t()) :: Changeset.t(t())
 | 
					  @spec validate_current_password(changeset(), String.t()) :: changeset()
 | 
				
			||||||
  def validate_current_password(changeset, password) do
 | 
					  def validate_current_password(changeset, password) do
 | 
				
			||||||
    if valid_password?(changeset.data, password),
 | 
					    if valid_password?(changeset.data, password),
 | 
				
			||||||
      do: changeset,
 | 
					      do: changeset,
 | 
				
			||||||
      else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))
 | 
					      else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  A changeset for changing the user's locale
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec locale_changeset(t() | changeset(), locale :: String.t() | nil) :: changeset()
 | 
				
			||||||
 | 
					  def locale_changeset(user_or_changeset, locale) do
 | 
				
			||||||
 | 
					    user_or_changeset
 | 
				
			||||||
 | 
					    |> cast(%{"locale" => locale}, [:locale])
 | 
				
			||||||
 | 
					    |> validate_required(:locale)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,8 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import Ecto.Query, warn: false
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
  import CanneryWeb.Gettext
 | 
					  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
 | 
				
			||||||
  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
 | 
					  alias Ecto.Multi
 | 
				
			||||||
  alias Ecto.{Changeset, Multi}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of shot_groups.
 | 
					  Returns the list of shot_groups.
 | 
				
			||||||
@@ -16,10 +15,50 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
      iex> list_shot_groups(%User{id: 123})
 | 
					      iex> list_shot_groups(%User{id: 123})
 | 
				
			||||||
      [%ShotGroup{}, ...]
 | 
					      [%ShotGroup{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_shot_groups("cool", %User{id: 123})
 | 
				
			||||||
 | 
					      [%ShotGroup{notes: "My cool shot group"}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_shot_groups(User.t()) :: [ShotGroup.t()]
 | 
					  @spec list_shot_groups(User.t()) :: [ShotGroup.t()]
 | 
				
			||||||
  def list_shot_groups(%User{id: user_id}) do
 | 
					  @spec list_shot_groups(search :: nil | String.t(), User.t()) :: [ShotGroup.t()]
 | 
				
			||||||
    Repo.all(from(sg in ShotGroup, where: sg.user_id == ^user_id))
 | 
					  def list_shot_groups(search \\ nil, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_shot_groups(search, %{id: user_id}) 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:
 | 
				
			||||||
 | 
					          fragment(
 | 
				
			||||||
 | 
					            "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					            sg.search,
 | 
				
			||||||
 | 
					            ^trimmed_search
 | 
				
			||||||
 | 
					          ) or
 | 
				
			||||||
 | 
					            fragment(
 | 
				
			||||||
 | 
					              "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					              ag.search,
 | 
				
			||||||
 | 
					              ^trimmed_search
 | 
				
			||||||
 | 
					            ) or
 | 
				
			||||||
 | 
					            fragment(
 | 
				
			||||||
 | 
					              "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					              at.search,
 | 
				
			||||||
 | 
					              ^trimmed_search
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        order_by: {
 | 
				
			||||||
 | 
					          :desc,
 | 
				
			||||||
 | 
					          fragment(
 | 
				
			||||||
 | 
					            "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
 | 
				
			||||||
 | 
					            sg.search,
 | 
				
			||||||
 | 
					            ^trimmed_search
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -59,33 +98,31 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_shot_group(attrs :: map(), User.t(), AmmoGroup.t()) ::
 | 
					  @spec create_shot_group(attrs :: map(), User.t(), AmmoGroup.t()) ::
 | 
				
			||||||
          {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t()) | nil}
 | 
					          {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset() | nil}
 | 
				
			||||||
  def create_shot_group(
 | 
					  def create_shot_group(attrs, user, ammo_group) do
 | 
				
			||||||
        attrs,
 | 
					    Multi.new()
 | 
				
			||||||
        %User{id: user_id},
 | 
					    |> Multi.insert(
 | 
				
			||||||
        %AmmoGroup{id: ammo_group_id, count: ammo_group_count, user_id: user_id} = ammo_group
 | 
					      :create_shot_group,
 | 
				
			||||||
      ) do
 | 
					      %ShotGroup{} |> ShotGroup.create_changeset(user, ammo_group, attrs)
 | 
				
			||||||
    attrs = attrs |> Map.merge(%{"user_id" => user_id, "ammo_group_id" => ammo_group_id})
 | 
					    )
 | 
				
			||||||
    changeset = %ShotGroup{} |> ShotGroup.create_changeset(attrs)
 | 
					    |> Multi.run(
 | 
				
			||||||
    shot_group_count = changeset |> Changeset.get_field(:count)
 | 
					      :ammo_group,
 | 
				
			||||||
 | 
					      fn repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
 | 
				
			||||||
    if shot_group_count > ammo_group_count do
 | 
					        {:ok,
 | 
				
			||||||
      error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count)
 | 
					         repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)}
 | 
				
			||||||
      changeset = changeset |> Changeset.add_error(:count, error)
 | 
					 | 
				
			||||||
      {:error, changeset}
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      Multi.new()
 | 
					 | 
				
			||||||
      |> Multi.insert(:create_shot_group, changeset)
 | 
					 | 
				
			||||||
      |> Multi.update(
 | 
					 | 
				
			||||||
        :update_ammo_group,
 | 
					 | 
				
			||||||
        ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count})
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      |> Repo.transaction()
 | 
					 | 
				
			||||||
      |> case do
 | 
					 | 
				
			||||||
        {:ok, %{create_shot_group: shot_group}} -> {:ok, shot_group}
 | 
					 | 
				
			||||||
        {:error, :create_shot_group, changeset, _changes_so_far} -> {:error, changeset}
 | 
					 | 
				
			||||||
        {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> Multi.update(
 | 
				
			||||||
 | 
					      :update_ammo_group,
 | 
				
			||||||
 | 
					      fn %{create_shot_group: %{count: shot_group_count}, ammo_group: %{count: ammo_group_count}} ->
 | 
				
			||||||
 | 
					        ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count})
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> Repo.transaction()
 | 
				
			||||||
 | 
					    |> case do
 | 
				
			||||||
 | 
					      {:ok, %{create_shot_group: shot_group}} -> {:ok, shot_group}
 | 
				
			||||||
 | 
					      {:error, :create_shot_group, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
 | 
					      {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -102,44 +139,40 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_shot_group(ShotGroup.t(), attrs :: map(), User.t()) ::
 | 
					  @spec update_shot_group(ShotGroup.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t()) | nil}
 | 
					          {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset() | nil}
 | 
				
			||||||
  def update_shot_group(
 | 
					  def update_shot_group(
 | 
				
			||||||
        %ShotGroup{count: count, user_id: user_id, ammo_group_id: ammo_group_id} = shot_group,
 | 
					        %ShotGroup{count: count, user_id: user_id} = shot_group,
 | 
				
			||||||
        attrs,
 | 
					        attrs,
 | 
				
			||||||
        %User{id: user_id} = user
 | 
					        %User{id: user_id} = user
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    %{count: ammo_group_count, user_id: ^user_id} =
 | 
					    Multi.new()
 | 
				
			||||||
      ammo_group = ammo_group_id |> Ammo.get_ammo_group!(user)
 | 
					    |> Multi.update(
 | 
				
			||||||
 | 
					      :update_shot_group,
 | 
				
			||||||
    changeset = shot_group |> ShotGroup.update_changeset(attrs)
 | 
					      shot_group |> ShotGroup.update_changeset(user, attrs)
 | 
				
			||||||
    new_shot_group_count = changeset |> Changeset.get_field(:count)
 | 
					    )
 | 
				
			||||||
    shot_diff_to_add = new_shot_group_count - count
 | 
					    |> Multi.run(
 | 
				
			||||||
 | 
					      :ammo_group,
 | 
				
			||||||
    cond do
 | 
					      fn repo, %{update_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
 | 
				
			||||||
      shot_diff_to_add > ammo_group_count ->
 | 
					        {:ok,
 | 
				
			||||||
        error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count)
 | 
					         repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)}
 | 
				
			||||||
        changeset = changeset |> Changeset.add_error(:count, error)
 | 
					      end
 | 
				
			||||||
        {:error, changeset}
 | 
					    )
 | 
				
			||||||
 | 
					    |> Multi.update(
 | 
				
			||||||
      new_shot_group_count <= 0 ->
 | 
					      :update_ammo_group,
 | 
				
			||||||
        error = dgettext("errors", "Count must be at least 1")
 | 
					      fn %{
 | 
				
			||||||
        changeset = changeset |> Changeset.add_error(:count, error)
 | 
					           update_shot_group: %{count: new_count},
 | 
				
			||||||
        {:error, changeset}
 | 
					           ammo_group: %{count: ammo_group_count} = ammo_group
 | 
				
			||||||
 | 
					         } ->
 | 
				
			||||||
      true ->
 | 
					        shot_diff_to_add = new_count - count
 | 
				
			||||||
        Multi.new()
 | 
					        new_ammo_group_count = ammo_group_count - shot_diff_to_add
 | 
				
			||||||
        |> Multi.update(:update_shot_group, changeset)
 | 
					        ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count})
 | 
				
			||||||
        |> Multi.update(
 | 
					      end
 | 
				
			||||||
          :update_ammo_group,
 | 
					    )
 | 
				
			||||||
          ammo_group
 | 
					    |> Repo.transaction()
 | 
				
			||||||
          |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_diff_to_add})
 | 
					    |> case do
 | 
				
			||||||
        )
 | 
					      {:ok, %{update_shot_group: shot_group}} -> {:ok, shot_group}
 | 
				
			||||||
        |> Repo.transaction()
 | 
					      {:error, :update_shot_group, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
        |> case do
 | 
					      {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
				
			||||||
          {:ok, %{update_shot_group: shot_group}} -> {:ok, shot_group}
 | 
					 | 
				
			||||||
          {:error, :update_shot_group, changeset, _changes_so_far} -> {:error, changeset}
 | 
					 | 
				
			||||||
          {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -156,20 +189,29 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_shot_group(ShotGroup.t(), User.t()) ::
 | 
					  @spec delete_shot_group(ShotGroup.t(), User.t()) ::
 | 
				
			||||||
          {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t())}
 | 
					          {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset()}
 | 
				
			||||||
  def delete_shot_group(
 | 
					  def delete_shot_group(
 | 
				
			||||||
        %ShotGroup{count: count, user_id: user_id, ammo_group_id: ammo_group_id} = shot_group,
 | 
					        %ShotGroup{user_id: user_id} = shot_group,
 | 
				
			||||||
        %User{id: user_id} = user
 | 
					        %User{id: user_id}
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    %{count: ammo_group_count, user_id: ^user_id} =
 | 
					 | 
				
			||||||
      ammo_group = ammo_group_id |> Ammo.get_ammo_group!(user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Multi.new()
 | 
					    Multi.new()
 | 
				
			||||||
    |> Multi.delete(:delete_shot_group, shot_group)
 | 
					    |> Multi.delete(:delete_shot_group, shot_group)
 | 
				
			||||||
 | 
					    |> Multi.run(
 | 
				
			||||||
 | 
					      :ammo_group,
 | 
				
			||||||
 | 
					      fn repo, %{delete_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)}
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    |> Multi.update(
 | 
					    |> Multi.update(
 | 
				
			||||||
      :update_ammo_group,
 | 
					      :update_ammo_group,
 | 
				
			||||||
      ammo_group
 | 
					      fn %{
 | 
				
			||||||
      |> AmmoGroup.range_changeset(%{"count" => ammo_group_count + count})
 | 
					           delete_shot_group: %{count: count},
 | 
				
			||||||
 | 
					           ammo_group: %{count: ammo_group_count} = ammo_group
 | 
				
			||||||
 | 
					         } ->
 | 
				
			||||||
 | 
					        new_ammo_group_count = ammo_group_count + count
 | 
				
			||||||
 | 
					        ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count})
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
@@ -178,21 +220,4 @@ defmodule Cannery.ActivityLog do
 | 
				
			|||||||
      {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
					      {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Ecto.Changeset{}` for tracking shot_group changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_shot_group(shot_group)
 | 
					 | 
				
			||||||
      %Ecto.Changeset{data: %ShotGroup{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_shot_group(ShotGroup.t() | ShotGroup.new_shot_group()) ::
 | 
					 | 
				
			||||||
          Changeset.t(ShotGroup.t() | ShotGroup.new_shot_group())
 | 
					 | 
				
			||||||
  @spec change_shot_group(ShotGroup.t() | ShotGroup.new_shot_group(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(ShotGroup.t() | ShotGroup.new_shot_group())
 | 
					 | 
				
			||||||
  def change_shot_group(%ShotGroup{} = shot_group, attrs \\ %{}) do
 | 
					 | 
				
			||||||
    shot_group |> ShotGroup.update_changeset(attrs)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,19 @@ defmodule Cannery.ActivityLog.ShotGroup do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use Ecto.Schema
 | 
					  use Ecto.Schema
 | 
				
			||||||
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  import Ecto.Changeset
 | 
					  import Ecto.Changeset
 | 
				
			||||||
  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup}
 | 
					  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo}
 | 
				
			||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :count,
 | 
				
			||||||
 | 
					             :date,
 | 
				
			||||||
 | 
					             :notes,
 | 
				
			||||||
 | 
					             :ammo_group_id
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "shot_groups" do
 | 
					  schema "shot_groups" do
 | 
				
			||||||
@@ -35,23 +44,85 @@ defmodule Cannery.ActivityLog.ShotGroup do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_shot_group :: %ShotGroup{}
 | 
					  @type new_shot_group :: %ShotGroup{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_shot_group())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_shot_group(), attrs :: map()) :: Changeset.t(new_shot_group())
 | 
					  @spec create_changeset(
 | 
				
			||||||
  def create_changeset(shot_group, attrs) do
 | 
					          new_shot_group(),
 | 
				
			||||||
 | 
					          User.t() | any(),
 | 
				
			||||||
 | 
					          AmmoGroup.t() | any(),
 | 
				
			||||||
 | 
					          attrs :: map()
 | 
				
			||||||
 | 
					        ) :: changeset()
 | 
				
			||||||
 | 
					  def create_changeset(
 | 
				
			||||||
 | 
					        shot_group,
 | 
				
			||||||
 | 
					        %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
 | 
				
			||||||
    shot_group
 | 
					    shot_group
 | 
				
			||||||
    |> cast(attrs, [:count, :notes, :date, :ammo_group_id, :user_id])
 | 
					    |> change(user_id: user_id)
 | 
				
			||||||
 | 
					    |> change(ammo_group_id: ammo_group_id)
 | 
				
			||||||
 | 
					    |> cast(attrs, [:count, :notes, :date])
 | 
				
			||||||
    |> validate_number(:count, greater_than: 0)
 | 
					    |> validate_number(:count, greater_than: 0)
 | 
				
			||||||
    |> validate_required([:count, :ammo_group_id, :user_id])
 | 
					    |> validate_create_shot_group_count(ammo_group)
 | 
				
			||||||
 | 
					    |> validate_required([:count, :date, :ammo_group_id, :user_id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do
 | 
				
			||||||
  @spec update_changeset(t() | new_shot_group(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(t() | new_shot_group())
 | 
					 | 
				
			||||||
  def update_changeset(shot_group, attrs) do
 | 
					 | 
				
			||||||
    shot_group
 | 
					    shot_group
 | 
				
			||||||
    |> cast(attrs, [:count, :notes, :date])
 | 
					    |> cast(attrs, [:count, :notes, :date])
 | 
				
			||||||
    |> validate_number(:count, greater_than: 0)
 | 
					    |> validate_number(:count, greater_than: 0)
 | 
				
			||||||
    |> validate_required([:count])
 | 
					    |> validate_required([:count, :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
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					    shot_group
 | 
				
			||||||
 | 
					    |> cast(attrs, [:count, :notes, :date])
 | 
				
			||||||
 | 
					    |> validate_number(:count, greater_than: 0)
 | 
				
			||||||
 | 
					    |> validate_required([:count, :date])
 | 
				
			||||||
 | 
					    |> validate_update_shot_group_count(shot_group, user)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					        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 ->
 | 
				
			||||||
 | 
					        changeset
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,9 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  The Ammo context.
 | 
					  The Ammo context.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  import Ecto.Query, warn: false
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
  alias Cannery.{Accounts.User, Containers, Repo}
 | 
					  alias Cannery.{Accounts.User, Containers, Containers.Container, Repo}
 | 
				
			||||||
  alias Cannery.ActivityLog.ShotGroup
 | 
					  alias Cannery.ActivityLog.ShotGroup
 | 
				
			||||||
  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
					  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
@@ -19,11 +20,57 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
      iex> list_ammo_types(%User{id: 123})
 | 
					      iex> list_ammo_types(%User{id: 123})
 | 
				
			||||||
      [%AmmoType{}, ...]
 | 
					      [%AmmoType{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_ammo_types("cool", %User{id: 123})
 | 
				
			||||||
 | 
					      [%AmmoType{name: "My cool ammo type"}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_ammo_types(User.t()) :: [AmmoType.t()]
 | 
					  @spec list_ammo_types(User.t()) :: [AmmoType.t()]
 | 
				
			||||||
  def list_ammo_types(%User{id: user_id}),
 | 
					  @spec list_ammo_types(search :: nil | String.t(), User.t()) :: [AmmoType.t()]
 | 
				
			||||||
 | 
					  def list_ammo_types(search \\ nil, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "",
 | 
				
			||||||
    do: Repo.all(from at in AmmoType, where: at.user_id == ^user_id, order_by: at.name)
 | 
					    do: Repo.all(from at in AmmoType, where: at.user_id == ^user_id, order_by: at.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_types(search, %{id: user_id}) when search |> is_binary() do
 | 
				
			||||||
 | 
					    trimmed_search = String.trim(search)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Repo.all(
 | 
				
			||||||
 | 
					      from at in AmmoType,
 | 
				
			||||||
 | 
					        where: at.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 """
 | 
				
			||||||
 | 
					  Returns a count of ammo_types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_ammo_types_count!(%User{id: 123})
 | 
				
			||||||
 | 
					      3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_ammo_types_count!(User.t()) :: integer()
 | 
				
			||||||
 | 
					  def get_ammo_types_count!(%User{id: user_id}) do
 | 
				
			||||||
 | 
					    Repo.one(
 | 
				
			||||||
 | 
					      from at in AmmoType,
 | 
				
			||||||
 | 
					        where: at.user_id == ^user_id,
 | 
				
			||||||
 | 
					        select: count(at.id),
 | 
				
			||||||
 | 
					        distinct: true
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets a single ammo_type.
 | 
					  Gets a single ammo_type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,7 +128,7 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_round_count_for_ammo_type(123, %User{id: 123})
 | 
					      iex> get_round_count_for_ammo_type(123, %User{id: 123})
 | 
				
			||||||
      %AmmoType{}
 | 
					      35
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_round_count_for_ammo_type(456, %User{id: 123})
 | 
					      iex> get_round_count_for_ammo_type(456, %User{id: 123})
 | 
				
			||||||
      ** (Ecto.NoResultsError)
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
@@ -107,7 +154,7 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_used_count_for_ammo_type(123, %User{id: 123})
 | 
					      iex> get_used_count_for_ammo_type(123, %User{id: 123})
 | 
				
			||||||
      %AmmoType{}
 | 
					      35
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_used_count_for_ammo_type(456, %User{id: 123})
 | 
					      iex> get_used_count_for_ammo_type(456, %User{id: 123})
 | 
				
			||||||
      ** (Ecto.NoResultsError)
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
@@ -126,6 +173,29 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
    ) || 0
 | 
					    ) || 0
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Gets the total number of ammo ever bought for an ammo type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Raises `Ecto.NoResultsError` if the Ammo type does not exist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_historical_count_for_ammo_type(123, %User{id: 123})
 | 
				
			||||||
 | 
					      %AmmoType{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_historical_count_for_ammo_type(456, %User{id: 123})
 | 
				
			||||||
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_historical_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_historical_count_for_ammo_type(
 | 
				
			||||||
 | 
					        %AmmoType{user_id: user_id} = ammo_type,
 | 
				
			||||||
 | 
					        %User{id: user_id} = user
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    get_round_count_for_ammo_type(ammo_type, user) +
 | 
				
			||||||
 | 
					      get_used_count_for_ammo_type(ammo_type, user)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Creates a ammo_type.
 | 
					  Creates a ammo_type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -139,12 +209,9 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_ammo_type(attrs :: map(), User.t()) ::
 | 
					  @spec create_ammo_type(attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoType.t()} | {:error, Changeset.t(AmmoType.new_ammo_type())}
 | 
					          {:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
 | 
				
			||||||
  def create_ammo_type(attrs \\ %{}, %User{id: user_id}) do
 | 
					  def create_ammo_type(attrs \\ %{}, %User{} = user),
 | 
				
			||||||
    %AmmoType{}
 | 
					    do: %AmmoType{} |> AmmoType.create_changeset(user, attrs) |> Repo.insert()
 | 
				
			||||||
    |> AmmoType.create_changeset(attrs |> Map.put("user_id", user_id))
 | 
					 | 
				
			||||||
    |> Repo.insert()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Updates a ammo_type.
 | 
					  Updates a ammo_type.
 | 
				
			||||||
@@ -159,7 +226,7 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_ammo_type(AmmoType.t(), attrs :: map(), User.t()) ::
 | 
					  @spec update_ammo_type(AmmoType.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoType.t()} | {:error, Changeset.t(AmmoType.t())}
 | 
					          {:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
 | 
				
			||||||
  def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}),
 | 
					  def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}),
 | 
				
			||||||
    do: ammo_type |> AmmoType.update_changeset(attrs) |> Repo.update()
 | 
					    do: ammo_type |> AmmoType.update_changeset(attrs) |> Repo.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,7 +243,7 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_ammo_type(AmmoType.t(), User.t()) ::
 | 
					  @spec delete_ammo_type(AmmoType.t(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoType.t()} | {:error, Changeset.t(AmmoType.t())}
 | 
					          {:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
 | 
				
			||||||
  def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
 | 
					  def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
 | 
				
			||||||
    do: ammo_type |> Repo.delete()
 | 
					    do: ammo_type |> Repo.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,22 +260,6 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  def delete_ammo_type!(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
 | 
					  def delete_ammo_type!(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}),
 | 
				
			||||||
    do: ammo_type |> Repo.delete!()
 | 
					    do: ammo_type |> Repo.delete!()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking ammo_type changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_ammo_type(ammo_type)
 | 
					 | 
				
			||||||
      %Changeset{data: %AmmoType{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_ammo_type(AmmoType.t() | AmmoType.new_ammo_type()) ::
 | 
					 | 
				
			||||||
          Changeset.t(AmmoType.t() | AmmoType.new_ammo_type())
 | 
					 | 
				
			||||||
  @spec change_ammo_type(AmmoType.t() | AmmoType.new_ammo_type(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(AmmoType.t() | AmmoType.new_ammo_type())
 | 
					 | 
				
			||||||
  def change_ammo_type(%AmmoType{} = ammo_type, attrs \\ %{}),
 | 
					 | 
				
			||||||
    do: AmmoType.update_changeset(ammo_type, attrs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of ammo_groups for a user and type.
 | 
					  Returns the list of ammo_groups for a user and type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -219,7 +270,15 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_ammo_groups_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
 | 
					  @spec list_ammo_groups_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
  def list_ammo_groups_for_type(%AmmoType{id: ammo_type_id, user_id: user_id}, %User{id: user_id}) do
 | 
					  @spec list_ammo_groups_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
 | 
				
			||||||
 | 
					          [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_type(ammo_type, user, include_empty \\ false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        true = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
    Repo.all(
 | 
					    Repo.all(
 | 
				
			||||||
      from ag in AmmoGroup,
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
        left_join: sg in assoc(ag, :shot_groups),
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
@@ -230,35 +289,221 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        false = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.all(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: not (ag.count == 0),
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg],
 | 
				
			||||||
 | 
					        order_by: ag.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of ammo_groups for a user.
 | 
					  Returns the list of ammo_groups for a user and container.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_ammo_groups_for_container(%AmmoType{id: 123}, %User{id: 123})
 | 
				
			||||||
 | 
					      [%AmmoGroup{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec list_ammo_groups_for_container(Container.t(), User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  @spec list_ammo_groups_for_container(Container.t(), User.t(), include_empty :: boolean()) ::
 | 
				
			||||||
 | 
					          [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_container(container, user, include_empty \\ false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_container(
 | 
				
			||||||
 | 
					        %Container{id: container_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        true = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.all(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
 | 
					        where: ag.container_id == ^container_id,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg],
 | 
				
			||||||
 | 
					        order_by: ag.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def list_ammo_groups_for_container(
 | 
				
			||||||
 | 
					        %Container{id: container_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        false = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.all(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
 | 
					        where: ag.container_id == ^container_id,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: not (ag.count == 0),
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg],
 | 
				
			||||||
 | 
					        order_by: ag.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the count of ammo_groups for an ammo type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123})
 | 
				
			||||||
 | 
					      3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123}, true)
 | 
				
			||||||
 | 
					      5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
 | 
				
			||||||
 | 
					          [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  def get_ammo_groups_count_for_type(ammo_type, user, include_empty \\ false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_ammo_groups_count_for_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        true = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        distinct: true,
 | 
				
			||||||
 | 
					        select: count(ag.id)
 | 
				
			||||||
 | 
					    ) || 0
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_ammo_groups_count_for_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id},
 | 
				
			||||||
 | 
					        false = _include_empty
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        where: not (ag.count == 0),
 | 
				
			||||||
 | 
					        distinct: true,
 | 
				
			||||||
 | 
					        select: count(ag.id)
 | 
				
			||||||
 | 
					    ) || 0
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the count of used ammo_groups for an ammo type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_used_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123})
 | 
				
			||||||
 | 
					      3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
 | 
					  def get_used_ammo_groups_count_for_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id}
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        where: ag.count == 0,
 | 
				
			||||||
 | 
					        distinct: true,
 | 
				
			||||||
 | 
					        select: count(ag.id)
 | 
				
			||||||
 | 
					    ) || 0
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the list of ammo_groups.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> list_ammo_groups(%User{id: 123})
 | 
					      iex> list_ammo_groups(%User{id: 123})
 | 
				
			||||||
      [%AmmoGroup{}, ...]
 | 
					      [%AmmoGroup{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_ammo_groups("cool", true, %User{id: 123})
 | 
				
			||||||
 | 
					      [%AmmoGroup{notes: "My cool ammo group"}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_ammo_groups(User.t()) :: [AmmoGroup.t()]
 | 
					  @spec list_ammo_groups(User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
  @spec list_ammo_groups(User.t(), include_empty :: boolean()) :: [AmmoGroup.t()]
 | 
					  @spec list_ammo_groups(search :: nil | String.t(), User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
  def list_ammo_groups(%User{id: user_id}, include_empty \\ false) do
 | 
					  @spec list_ammo_groups(search :: nil | String.t(), include_empty :: boolean(), User.t()) ::
 | 
				
			||||||
    if include_empty do
 | 
					          [AmmoGroup.t()]
 | 
				
			||||||
      from ag in AmmoGroup,
 | 
					  def list_ammo_groups(search \\ nil, include_empty \\ false, %{id: user_id}) do
 | 
				
			||||||
        left_join: sg in assoc(ag, :shot_groups),
 | 
					    from(
 | 
				
			||||||
        where: ag.user_id == ^user_id,
 | 
					      ag in AmmoGroup,
 | 
				
			||||||
        preload: [shot_groups: sg],
 | 
					      as: :ag,
 | 
				
			||||||
        order_by: ag.id
 | 
					      left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
    else
 | 
					      as: :sg,
 | 
				
			||||||
      from ag in AmmoGroup,
 | 
					      join: at in assoc(ag, :ammo_type),
 | 
				
			||||||
        left_join: sg in assoc(ag, :shot_groups),
 | 
					      as: :at,
 | 
				
			||||||
        where: ag.user_id == ^user_id,
 | 
					      join: c in assoc(ag, :container),
 | 
				
			||||||
        where: not (ag.count == 0),
 | 
					      as: :c,
 | 
				
			||||||
        preload: [shot_groups: sg],
 | 
					      left_join: t in assoc(c, :tags),
 | 
				
			||||||
        order_by: ag.id
 | 
					      as: :t,
 | 
				
			||||||
    end
 | 
					      where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					      preload: [shot_groups: sg, ammo_type: at, container: {c, tags: t}],
 | 
				
			||||||
 | 
					      order_by: ag.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> list_ammo_groups_include_empty(include_empty)
 | 
				
			||||||
 | 
					    |> list_ammo_groups_search(search)
 | 
				
			||||||
    |> Repo.all()
 | 
					    |> Repo.all()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_ammo_groups_include_empty(query, true), do: query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_ammo_groups_include_empty(query, false) do
 | 
				
			||||||
 | 
					    query |> where([ag], not (ag.count == 0))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_ammo_groups_search(query, nil), do: query
 | 
				
			||||||
 | 
					  defp list_ammo_groups_search(query, ""), do: query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_ammo_groups_search(query, search) do
 | 
				
			||||||
 | 
					    trimmed_search = String.trim(search)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    query
 | 
				
			||||||
 | 
					    |> where(
 | 
				
			||||||
 | 
					      [ag: ag, at: at, c: c, t: t],
 | 
				
			||||||
 | 
					      fragment(
 | 
				
			||||||
 | 
					        "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					        ag.search,
 | 
				
			||||||
 | 
					        ^trimmed_search
 | 
				
			||||||
 | 
					      ) or
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					          at.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        ) or
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					          c.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        ) or
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					          t.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> order_by(
 | 
				
			||||||
 | 
					      [ag: ag],
 | 
				
			||||||
 | 
					      desc:
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
 | 
				
			||||||
 | 
					          ag.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of staged ammo_groups for a user.
 | 
					  Returns the list of staged ammo_groups for a user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -317,6 +562,17 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
    |> Enum.sum()
 | 
					    |> Enum.sum()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the last entered shot group for an ammo group
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_last_used_shot_group(AmmoGroup.t()) :: ShotGroup.t() | nil
 | 
				
			||||||
 | 
					  def get_last_used_shot_group(%AmmoGroup{} = ammo_group) do
 | 
				
			||||||
 | 
					    ammo_group
 | 
				
			||||||
 | 
					    |> Repo.preload(:shot_groups)
 | 
				
			||||||
 | 
					    |> Map.fetch!(:shot_groups)
 | 
				
			||||||
 | 
					    |> Enum.max_by(fn %{date: date} -> date end, Date, fn -> nil end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Calculates the percentage remaining of an ammo group out of 100
 | 
					  Calculates the percentage remaining of an ammo group out of 100
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
@@ -327,11 +583,35 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
    ammo_group = ammo_group |> Repo.preload(:shot_groups)
 | 
					    ammo_group = ammo_group |> Repo.preload(:shot_groups)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shot_group_sum =
 | 
					    shot_group_sum =
 | 
				
			||||||
      ammo_group.shot_groups |> Enum.map(fn %{count: count} -> count end) |> Enum.sum()
 | 
					      ammo_group.shot_groups
 | 
				
			||||||
 | 
					      |> Enum.map(fn %{count: count} -> count end)
 | 
				
			||||||
 | 
					      |> Enum.sum()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    round(count / (count + shot_group_sum) * 100)
 | 
					    round(count / (count + shot_group_sum) * 100)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Gets the original count for an ammo group
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_original_count(AmmoGroup.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_original_count(%AmmoGroup{count: count} = ammo_group) do
 | 
				
			||||||
 | 
					    count + get_used_count(ammo_group)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Calculates the CPR for a single ammo group
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_cpr(AmmoGroup.t()) :: nil | float()
 | 
				
			||||||
 | 
					  def get_cpr(%AmmoGroup{price_paid: nil}), do: nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_cpr(%AmmoGroup{price_paid: price_paid} = ammo_group),
 | 
				
			||||||
 | 
					    do: calculate_cpr(price_paid, get_original_count(ammo_group))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec calculate_cpr(price_paid :: float() | nil, count :: integer()) :: float() | nil
 | 
				
			||||||
 | 
					  defp calculate_cpr(nil, _count), do: nil
 | 
				
			||||||
 | 
					  defp calculate_cpr(_price_paid, 0), do: nil
 | 
				
			||||||
 | 
					  defp calculate_cpr(price_paid, total_count), do: price_paid / total_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Creates multiple ammo_groups at once.
 | 
					  Creates multiple ammo_groups at once.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -346,22 +626,25 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_ammo_groups(attrs :: map(), multiplier :: non_neg_integer(), User.t()) ::
 | 
					  @spec create_ammo_groups(attrs :: map(), multiplier :: non_neg_integer(), User.t()) ::
 | 
				
			||||||
          {:ok, {count :: non_neg_integer(), [AmmoGroup.t()] | nil}}
 | 
					          {:ok, {count :: non_neg_integer(), [AmmoGroup.t()] | nil}}
 | 
				
			||||||
          | {:error, Changeset.t(AmmoGroup.new_ammo_group())}
 | 
					          | {:error, AmmoGroup.changeset()}
 | 
				
			||||||
  def create_ammo_groups(
 | 
					  def create_ammo_groups(
 | 
				
			||||||
        %{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
 | 
					        %{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
 | 
				
			||||||
        multiplier,
 | 
					        multiplier,
 | 
				
			||||||
        %User{id: user_id} = user
 | 
					        %User{} = user
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      when multiplier >= 1 and multiplier <= @ammo_group_create_limit do
 | 
					      when multiplier >= 1 and multiplier <= @ammo_group_create_limit and
 | 
				
			||||||
    # validate ammo type and container ids belong to user
 | 
					             not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do
 | 
				
			||||||
    _valid_ammo_type = get_ammo_type!(ammo_type_id, user)
 | 
					 | 
				
			||||||
    _valid_container = Containers.get_container!(container_id, user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
					    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    changesets =
 | 
					    changesets =
 | 
				
			||||||
      Enum.map(1..multiplier, fn _count ->
 | 
					      Enum.map(1..multiplier, fn _count ->
 | 
				
			||||||
        %AmmoGroup{} |> AmmoGroup.create_changeset(attrs |> Map.put("user_id", user_id))
 | 
					        %AmmoGroup{}
 | 
				
			||||||
 | 
					        |> AmmoGroup.create_changeset(
 | 
				
			||||||
 | 
					          get_ammo_type!(ammo_type_id, user),
 | 
				
			||||||
 | 
					          Containers.get_container!(container_id, user),
 | 
				
			||||||
 | 
					          user,
 | 
				
			||||||
 | 
					          attrs
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      end)
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do
 | 
					    if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do
 | 
				
			||||||
@@ -386,8 +669,27 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create_ammo_groups(invalid_attrs, _multiplier, _user) do
 | 
					  def create_ammo_groups(
 | 
				
			||||||
    {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(invalid_attrs)}
 | 
					        %{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
 | 
				
			||||||
 | 
					        _multiplier,
 | 
				
			||||||
 | 
					        user
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      %AmmoGroup{}
 | 
				
			||||||
 | 
					      |> AmmoGroup.create_changeset(
 | 
				
			||||||
 | 
					        get_ammo_type!(ammo_type_id, user),
 | 
				
			||||||
 | 
					        Containers.get_container!(container_id, user),
 | 
				
			||||||
 | 
					        user,
 | 
				
			||||||
 | 
					        attrs
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      |> Changeset.add_error(:multiplier, dgettext("errors", "Invalid multiplier"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:error, changeset}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create_ammo_groups(invalid_attrs, _multiplier, user) do
 | 
				
			||||||
 | 
					    {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(nil, nil, user, invalid_attrs)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -403,9 +705,13 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_ammo_group(AmmoGroup.t(), attrs :: map(), User.t()) ::
 | 
					  @spec update_ammo_group(AmmoGroup.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoGroup.t()} | {:error, Changeset.t(AmmoGroup.t())}
 | 
					          {:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()}
 | 
				
			||||||
  def update_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, attrs, %User{id: user_id}),
 | 
					  def update_ammo_group(
 | 
				
			||||||
    do: ammo_group |> AmmoGroup.update_changeset(attrs) |> Repo.update()
 | 
					        %AmmoGroup{user_id: user_id} = ammo_group,
 | 
				
			||||||
 | 
					        attrs,
 | 
				
			||||||
 | 
					        %User{id: user_id} = user
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      do: ammo_group |> AmmoGroup.update_changeset(attrs, user) |> Repo.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes a ammo_group.
 | 
					  Deletes a ammo_group.
 | 
				
			||||||
@@ -420,7 +726,7 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_ammo_group(AmmoGroup.t(), User.t()) ::
 | 
					  @spec delete_ammo_group(AmmoGroup.t(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoGroup.t()} | {:error, Changeset.t(AmmoGroup.t())}
 | 
					          {:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()}
 | 
				
			||||||
  def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
 | 
					  def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
 | 
				
			||||||
    do: ammo_group |> Repo.delete()
 | 
					    do: ammo_group |> Repo.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -436,18 +742,4 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  @spec delete_ammo_group!(AmmoGroup.t(), User.t()) :: AmmoGroup.t()
 | 
					  @spec delete_ammo_group!(AmmoGroup.t(), User.t()) :: AmmoGroup.t()
 | 
				
			||||||
  def delete_ammo_group!(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
 | 
					  def delete_ammo_group!(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}),
 | 
				
			||||||
    do: ammo_group |> Repo.delete!()
 | 
					    do: ammo_group |> Repo.delete!()
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking ammo_group changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_ammo_group(ammo_group)
 | 
					 | 
				
			||||||
      %Changeset{data: %AmmoGroup{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_ammo_group(AmmoGroup.t()) :: Changeset.t(AmmoGroup.t())
 | 
					 | 
				
			||||||
  @spec change_ammo_group(AmmoGroup.t(), attrs :: map()) :: Changeset.t(AmmoGroup.t())
 | 
					 | 
				
			||||||
  def change_ammo_group(%AmmoGroup{} = ammo_group, attrs \\ %{}),
 | 
					 | 
				
			||||||
    do: AmmoGroup.update_changeset(ammo_group, attrs)
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,11 +7,22 @@ defmodule Cannery.Ammo.AmmoGroup do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use Ecto.Schema
 | 
					  use Ecto.Schema
 | 
				
			||||||
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  import Ecto.Changeset
 | 
					  import Ecto.Changeset
 | 
				
			||||||
  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
					  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
				
			||||||
  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Containers.Container}
 | 
					  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Containers, Containers.Container}
 | 
				
			||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :count,
 | 
				
			||||||
 | 
					             :notes,
 | 
				
			||||||
 | 
					             :price_paid,
 | 
				
			||||||
 | 
					             :staged,
 | 
				
			||||||
 | 
					             :ammo_type_id,
 | 
				
			||||||
 | 
					             :container_id
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "ammo_groups" do
 | 
					  schema "ammo_groups" do
 | 
				
			||||||
@@ -19,6 +30,7 @@ defmodule Cannery.Ammo.AmmoGroup do
 | 
				
			|||||||
    field :notes, :string
 | 
					    field :notes, :string
 | 
				
			||||||
    field :price_paid, :float
 | 
					    field :price_paid, :float
 | 
				
			||||||
    field :staged, :boolean, default: false
 | 
					    field :staged, :boolean, default: false
 | 
				
			||||||
 | 
					    field :purchased_on, :date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :ammo_type, AmmoType
 | 
					    belongs_to :ammo_type, AmmoType
 | 
				
			||||||
    belongs_to :container, Container
 | 
					    belongs_to :container, Container
 | 
				
			||||||
@@ -35,6 +47,7 @@ defmodule Cannery.Ammo.AmmoGroup do
 | 
				
			|||||||
          notes: String.t() | nil,
 | 
					          notes: String.t() | nil,
 | 
				
			||||||
          price_paid: float() | nil,
 | 
					          price_paid: float() | nil,
 | 
				
			||||||
          staged: boolean(),
 | 
					          staged: boolean(),
 | 
				
			||||||
 | 
					          purchased_on: Date.t(),
 | 
				
			||||||
          ammo_type: AmmoType.t() | nil,
 | 
					          ammo_type: AmmoType.t() | nil,
 | 
				
			||||||
          ammo_type_id: AmmoType.id(),
 | 
					          ammo_type_id: AmmoType.id(),
 | 
				
			||||||
          container: Container.t() | nil,
 | 
					          container: Container.t() | nil,
 | 
				
			||||||
@@ -46,35 +59,72 @@ defmodule Cannery.Ammo.AmmoGroup do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_ammo_group :: %AmmoGroup{}
 | 
					  @type new_ammo_group :: %AmmoGroup{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_ammo_group())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_ammo_group(), attrs :: map()) :: Changeset.t(new_ammo_group())
 | 
					  @spec create_changeset(
 | 
				
			||||||
  def create_changeset(ammo_group, attrs) do
 | 
					          new_ammo_group(),
 | 
				
			||||||
 | 
					          AmmoType.t() | nil,
 | 
				
			||||||
 | 
					          Container.t() | nil,
 | 
				
			||||||
 | 
					          User.t(),
 | 
				
			||||||
 | 
					          attrs :: map()
 | 
				
			||||||
 | 
					        ) :: changeset()
 | 
				
			||||||
 | 
					  def create_changeset(
 | 
				
			||||||
 | 
					        ammo_group,
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id},
 | 
				
			||||||
 | 
					        %Container{id: container_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %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
 | 
				
			||||||
    ammo_group
 | 
					    ammo_group
 | 
				
			||||||
    |> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id, :user_id])
 | 
					    |> change(ammo_type_id: ammo_type_id)
 | 
				
			||||||
 | 
					    |> change(user_id: user_id)
 | 
				
			||||||
 | 
					    |> change(container_id: container_id)
 | 
				
			||||||
 | 
					    |> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on])
 | 
				
			||||||
    |> validate_number(:count, greater_than: 0)
 | 
					    |> validate_number(:count, greater_than: 0)
 | 
				
			||||||
    |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
 | 
					    |> validate_required([:count, :staged, :purchased_on, :ammo_type_id, :container_id, :user_id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Invalid changeset, used to prompt user to select ammo type and container
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def create_changeset(ammo_group, _invalid_ammo_type, _invalid_container, _invalid_user, attrs) do
 | 
				
			||||||
 | 
					    ammo_group
 | 
				
			||||||
 | 
					    |> cast(attrs, [:ammo_type_id, :container_id])
 | 
				
			||||||
 | 
					    |> validate_required([:ammo_type_id, :container_id])
 | 
				
			||||||
 | 
					    |> add_error(:invalid, dgettext("errors", "Please select an ammo type and container"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec update_changeset(t() | new_ammo_group(), attrs :: map()) ::
 | 
					  @spec update_changeset(t() | new_ammo_group(), attrs :: map(), User.t()) :: changeset()
 | 
				
			||||||
          Changeset.t(t() | new_ammo_group())
 | 
					  def update_changeset(ammo_group, attrs, user) do
 | 
				
			||||||
  def update_changeset(ammo_group, attrs) do
 | 
					 | 
				
			||||||
    ammo_group
 | 
					    ammo_group
 | 
				
			||||||
    |> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id])
 | 
					    |> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :container_id])
 | 
				
			||||||
    |> validate_number(:count, greater_than_or_equal_to: 0)
 | 
					    |> validate_number(:count, greater_than_or_equal_to: 0)
 | 
				
			||||||
    |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
 | 
					    |> validate_container_id(user)
 | 
				
			||||||
 | 
					    |> validate_required([:count, :staged, :purchased_on, :container_id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp validate_container_id(changeset, user) do
 | 
				
			||||||
 | 
					    container_id = changeset |> Changeset.get_field(:container_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if container_id do
 | 
				
			||||||
 | 
					      Containers.get_container!(container_id, user)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  This range changeset is used when "using up" ammo groups, and allows for
 | 
					  This range changeset is used when "using up" ammo groups, and allows for
 | 
				
			||||||
  updating the count to 0
 | 
					  updating the count to 0
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec range_changeset(t() | new_ammo_group(), attrs :: map()) ::
 | 
					  @spec range_changeset(t() | new_ammo_group(), attrs :: map()) :: changeset()
 | 
				
			||||||
          Changeset.t(t() | new_ammo_group())
 | 
					 | 
				
			||||||
  def range_changeset(ammo_group, attrs) do
 | 
					  def range_changeset(ammo_group, attrs) do
 | 
				
			||||||
    ammo_group
 | 
					    ammo_group
 | 
				
			||||||
    |> cast(attrs, [:count, :staged])
 | 
					    |> cast(attrs, [:count, :staged])
 | 
				
			||||||
    |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
 | 
					    |> validate_required([:count, :staged])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,31 @@ defmodule Cannery.Ammo.AmmoType do
 | 
				
			|||||||
  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
					  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
				
			||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :name,
 | 
				
			||||||
 | 
					             :desc,
 | 
				
			||||||
 | 
					             :bullet_type,
 | 
				
			||||||
 | 
					             :bullet_core,
 | 
				
			||||||
 | 
					             :cartridge,
 | 
				
			||||||
 | 
					             :caliber,
 | 
				
			||||||
 | 
					             :case_material,
 | 
				
			||||||
 | 
					             :jacket_type,
 | 
				
			||||||
 | 
					             :muzzle_velocity,
 | 
				
			||||||
 | 
					             :powder_type,
 | 
				
			||||||
 | 
					             :powder_grains_per_charge,
 | 
				
			||||||
 | 
					             :grains,
 | 
				
			||||||
 | 
					             :pressure,
 | 
				
			||||||
 | 
					             :primer_type,
 | 
				
			||||||
 | 
					             :firing_type,
 | 
				
			||||||
 | 
					             :tracer,
 | 
				
			||||||
 | 
					             :incendiary,
 | 
				
			||||||
 | 
					             :blank,
 | 
				
			||||||
 | 
					             :corrosive,
 | 
				
			||||||
 | 
					             :manufacturer,
 | 
				
			||||||
 | 
					             :upc
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "ammo_types" do
 | 
					  schema "ammo_types" do
 | 
				
			||||||
@@ -31,10 +56,10 @@ defmodule Cannery.Ammo.AmmoType do
 | 
				
			|||||||
    field :pressure, :string
 | 
					    field :pressure, :string
 | 
				
			||||||
    field :primer_type, :string
 | 
					    field :primer_type, :string
 | 
				
			||||||
    field :firing_type, :string
 | 
					    field :firing_type, :string
 | 
				
			||||||
    field :tracer, :boolean, null: false, default: false
 | 
					    field :tracer, :boolean, default: false
 | 
				
			||||||
    field :incendiary, :boolean, null: false, default: false
 | 
					    field :incendiary, :boolean, default: false
 | 
				
			||||||
    field :blank, :boolean, null: false, default: false
 | 
					    field :blank, :boolean, default: false
 | 
				
			||||||
    field :corrosive, :boolean, null: false, default: false
 | 
					    field :corrosive, :boolean, default: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    field :manufacturer, :string
 | 
					    field :manufacturer, :string
 | 
				
			||||||
    field :upc, :string
 | 
					    field :upc, :string
 | 
				
			||||||
@@ -77,6 +102,7 @@ defmodule Cannery.Ammo.AmmoType do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_ammo_type :: %AmmoType{}
 | 
					  @type new_ammo_type :: %AmmoType{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_ammo_type())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec changeset_fields() :: [atom()]
 | 
					  @spec changeset_fields() :: [atom()]
 | 
				
			||||||
  defp changeset_fields,
 | 
					  defp changeset_fields,
 | 
				
			||||||
@@ -105,19 +131,19 @@ defmodule Cannery.Ammo.AmmoType do
 | 
				
			|||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_ammo_type(), attrs :: map()) :: Changeset.t(new_ammo_type())
 | 
					  @spec create_changeset(new_ammo_type(), User.t(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def create_changeset(ammo_type, attrs) do
 | 
					  def create_changeset(ammo_type, %User{id: user_id}, attrs) do
 | 
				
			||||||
    ammo_type
 | 
					    ammo_type
 | 
				
			||||||
    |> cast(attrs, [:user_id | changeset_fields()])
 | 
					    |> change(user_id: user_id)
 | 
				
			||||||
 | 
					    |> cast(attrs, changeset_fields())
 | 
				
			||||||
    |> validate_required([:name, :user_id])
 | 
					    |> validate_required([:name, :user_id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec update_changeset(t() | new_ammo_type(), attrs :: map()) ::
 | 
					  @spec update_changeset(t() | new_ammo_type(), attrs :: map()) :: changeset()
 | 
				
			||||||
          Changeset.t(t() | new_ammo_type())
 | 
					 | 
				
			||||||
  def update_changeset(ammo_type, attrs) do
 | 
					  def update_changeset(ammo_type, attrs) do
 | 
				
			||||||
    ammo_type
 | 
					    ammo_type
 | 
				
			||||||
    |> cast(attrs, changeset_fields())
 | 
					    |> cast(attrs, changeset_fields())
 | 
				
			||||||
    |> validate_required([:name, :user_id])
 | 
					    |> validate_required(:name)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,16 +17,74 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
      iex> list_containers(%User{id: 123})
 | 
					      iex> list_containers(%User{id: 123})
 | 
				
			||||||
      [%Container{}, ...]
 | 
					      [%Container{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_containers("cool", %User{id: 123})
 | 
				
			||||||
 | 
					      [%Container{name: "my cool container"}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_containers(User.t()) :: [Container.t()]
 | 
					  @spec list_containers(User.t()) :: [Container.t()]
 | 
				
			||||||
  def list_containers(%User{id: user_id}) do
 | 
					  @spec list_containers(search :: nil | String.t(), User.t()) :: [Container.t()]
 | 
				
			||||||
    Repo.all(
 | 
					  def list_containers(search \\ nil, %User{id: user_id}) do
 | 
				
			||||||
 | 
					    from(c in Container,
 | 
				
			||||||
 | 
					      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]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> list_containers_search(search)
 | 
				
			||||||
 | 
					    |> Repo.all()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_containers_search(query, nil), do: query
 | 
				
			||||||
 | 
					  defp list_containers_search(query, ""), do: query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_containers_search(query, search) do
 | 
				
			||||||
 | 
					    trimmed_search = String.trim(search)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    query
 | 
				
			||||||
 | 
					    |> where(
 | 
				
			||||||
 | 
					      [c: c, t: t],
 | 
				
			||||||
 | 
					      fragment(
 | 
				
			||||||
 | 
					        "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					        c.search,
 | 
				
			||||||
 | 
					        ^trimmed_search
 | 
				
			||||||
 | 
					      ) or
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "? @@ websearch_to_tsquery('english', ?)",
 | 
				
			||||||
 | 
					          t.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> order_by(
 | 
				
			||||||
 | 
					      [c: c],
 | 
				
			||||||
 | 
					      desc:
 | 
				
			||||||
 | 
					        fragment(
 | 
				
			||||||
 | 
					          "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
 | 
				
			||||||
 | 
					          c.search,
 | 
				
			||||||
 | 
					          ^trimmed_search
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns a count of containers.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_containers_count!(%User{id: 123})
 | 
				
			||||||
 | 
					      3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_containers_count!(User.t()) :: integer()
 | 
				
			||||||
 | 
					  def get_containers_count!(%User{id: user_id}) do
 | 
				
			||||||
 | 
					    Repo.one(
 | 
				
			||||||
      from c in Container,
 | 
					      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.user_id == ^user_id,
 | 
				
			||||||
        order_by: c.name,
 | 
					        select: count(c.id),
 | 
				
			||||||
        preload: [tags: t, ammo_groups: ag]
 | 
					        distinct: true
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -70,10 +128,9 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_container(attrs :: map(), User.t()) ::
 | 
					  @spec create_container(attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, Container.t()} | {:error, Changeset.t(Container.new_container())}
 | 
					          {:ok, Container.t()} | {:error, Container.changeset()}
 | 
				
			||||||
  def create_container(attrs, %User{id: user_id}) do
 | 
					  def create_container(attrs, %User{} = user) do
 | 
				
			||||||
    attrs = attrs |> Map.put("user_id", user_id)
 | 
					    %Container{} |> Container.create_changeset(user, attrs) |> Repo.insert()
 | 
				
			||||||
    %Container{} |> Container.create_changeset(attrs) |> Repo.insert()
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -89,7 +146,7 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_container(Container.t(), User.t(), attrs :: map()) ::
 | 
					  @spec update_container(Container.t(), User.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, Container.t()} | {:error, Changeset.t(Container.t())}
 | 
					          {:ok, Container.t()} | {:error, Container.changeset()}
 | 
				
			||||||
  def update_container(%Container{user_id: user_id} = container, %User{id: user_id}, attrs) do
 | 
					  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()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -107,7 +164,7 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_container(Container.t(), User.t()) ::
 | 
					  @spec delete_container(Container.t(), User.t()) ::
 | 
				
			||||||
          {:ok, Container.t()} | {:error, Changeset.t(Container.t())}
 | 
					          {:ok, Container.t()} | {:error, Container.changeset()}
 | 
				
			||||||
  def delete_container(%Container{user_id: user_id} = container, %User{id: user_id}) do
 | 
					  def delete_container(%Container{user_id: user_id} = container, %User{id: user_id}) do
 | 
				
			||||||
    Repo.one(
 | 
					    Repo.one(
 | 
				
			||||||
      from ag in AmmoGroup,
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
@@ -122,7 +179,7 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
        error = dgettext("errors", "Container must be empty before deleting")
 | 
					        error = dgettext("errors", "Container must be empty before deleting")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        container
 | 
					        container
 | 
				
			||||||
        |> change_container()
 | 
					        |> Container.update_changeset(%{})
 | 
				
			||||||
        |> Changeset.add_error(:ammo_groups, error)
 | 
					        |> Changeset.add_error(:ammo_groups, error)
 | 
				
			||||||
        |> Changeset.apply_action(:delete)
 | 
					        |> Changeset.apply_action(:delete)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
@@ -143,25 +200,6 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
    container
 | 
					    container
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking container changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_container(container)
 | 
					 | 
				
			||||||
      %Changeset{data: %Container{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_container(%Changeset{})
 | 
					 | 
				
			||||||
      %Changeset{data: %Container{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_container(Container.t() | Container.new_container()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Container.t() | Container.new_container())
 | 
					 | 
				
			||||||
  @spec change_container(Container.t() | Container.new_container(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Container.t() | Container.new_container())
 | 
					 | 
				
			||||||
  def change_container(container, attrs \\ %{}),
 | 
					 | 
				
			||||||
    do: container |> Container.update_changeset(attrs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Adds a tag to a container
 | 
					  Adds a tag to a container
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -173,14 +211,11 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec add_tag!(Container.t(), Tag.t(), User.t()) :: ContainerTag.t()
 | 
					  @spec add_tag!(Container.t(), Tag.t(), User.t()) :: ContainerTag.t()
 | 
				
			||||||
  def add_tag!(
 | 
					  def add_tag!(
 | 
				
			||||||
        %Container{id: container_id, user_id: user_id},
 | 
					        %Container{user_id: user_id} = container,
 | 
				
			||||||
        %Tag{id: tag_id, user_id: user_id},
 | 
					        %Tag{user_id: user_id} = tag,
 | 
				
			||||||
        %User{id: user_id}
 | 
					        %User{id: user_id}
 | 
				
			||||||
      ) do
 | 
					      ),
 | 
				
			||||||
    %ContainerTag{}
 | 
					      do: %ContainerTag{} |> ContainerTag.create_changeset(tag, container) |> Repo.insert!()
 | 
				
			||||||
    |> ContainerTag.changeset(%{"container_id" => container_id, "tag_id" => tag_id})
 | 
					 | 
				
			||||||
    |> Repo.insert!()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Removes a tag from a container
 | 
					  Removes a tag from a container
 | 
				
			||||||
@@ -207,6 +242,19 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
    if count == 0, do: raise("could not delete container tag"), else: count
 | 
					    if count == 0, do: raise("could not delete container tag"), else: count
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns number of rounds in container. If data is already preloaded, then
 | 
				
			||||||
 | 
					  there will be no db hit.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @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()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns number of rounds in container. If data is already preloaded, then
 | 
					  Returns number of rounds in container. If data is already preloaded, then
 | 
				
			||||||
  there will be no db hit.
 | 
					  there will be no db hit.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,15 @@ defmodule Cannery.Containers.Container do
 | 
				
			|||||||
  alias Cannery.Containers.{Container, ContainerTag}
 | 
					  alias Cannery.Containers.{Container, ContainerTag}
 | 
				
			||||||
  alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag}
 | 
					  alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :name,
 | 
				
			||||||
 | 
					             :desc,
 | 
				
			||||||
 | 
					             :location,
 | 
				
			||||||
 | 
					             :type,
 | 
				
			||||||
 | 
					             :tags
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "containers" do
 | 
					  schema "containers" do
 | 
				
			||||||
@@ -40,21 +49,22 @@ defmodule Cannery.Containers.Container do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_container :: %Container{}
 | 
					  @type new_container :: %Container{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_container())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_container(), attrs :: map()) :: Changeset.t(new_container())
 | 
					  @spec create_changeset(new_container(), User.t(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def create_changeset(container, attrs) do
 | 
					  def create_changeset(container, %User{id: user_id}, attrs) do
 | 
				
			||||||
    container
 | 
					 | 
				
			||||||
    |> cast(attrs, [:name, :desc, :type, :location, :user_id])
 | 
					 | 
				
			||||||
    |> validate_required([:name, :type, :user_id])
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc false
 | 
					 | 
				
			||||||
  @spec update_changeset(t() | new_container(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(t() | new_container())
 | 
					 | 
				
			||||||
  def update_changeset(container, attrs) do
 | 
					 | 
				
			||||||
    container
 | 
					    container
 | 
				
			||||||
 | 
					    |> change(user_id: user_id)
 | 
				
			||||||
    |> cast(attrs, [:name, :desc, :type, :location])
 | 
					    |> cast(attrs, [:name, :desc, :type, :location])
 | 
				
			||||||
    |> validate_required([:name, :type, :user_id])
 | 
					    |> validate_required([:name, :type, :user_id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc false
 | 
				
			||||||
 | 
					  @spec update_changeset(t() | new_container(), attrs :: map()) :: changeset()
 | 
				
			||||||
 | 
					  def update_changeset(container, attrs) do
 | 
				
			||||||
 | 
					    container
 | 
				
			||||||
 | 
					    |> cast(attrs, [:name, :desc, :type, :location])
 | 
				
			||||||
 | 
					    |> validate_required([:name, :type])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,12 +29,18 @@ defmodule Cannery.Containers.ContainerTag do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_container_tag :: %ContainerTag{}
 | 
					  @type new_container_tag :: %ContainerTag{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_container_tag())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec changeset(new_container_tag(), attrs :: map()) :: Changeset.t(new_container_tag())
 | 
					  @spec create_changeset(new_container_tag(), Tag.t(), Container.t()) :: changeset()
 | 
				
			||||||
  def changeset(container_tag, attrs) do
 | 
					  def create_changeset(
 | 
				
			||||||
 | 
					        container_tag,
 | 
				
			||||||
 | 
					        %Tag{id: tag_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %Container{id: container_id, user_id: user_id}
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
    container_tag
 | 
					    container_tag
 | 
				
			||||||
    |> cast(attrs, [:tag_id, :container_id])
 | 
					    |> change(tag_id: tag_id)
 | 
				
			||||||
 | 
					    |> change(container_id: container_id)
 | 
				
			||||||
    |> validate_required([:tag_id, :container_id])
 | 
					    |> validate_required([:tag_id, :container_id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@ defmodule Cannery.Invites do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  import Ecto.Query, warn: false
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
  alias Cannery.{Accounts.User, Invites.Invite, Repo}
 | 
					  alias Cannery.{Accounts.User, Invites.Invite, Repo}
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @invite_token_length 20
 | 
					  @invite_token_length 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -99,16 +98,14 @@ defmodule Cannery.Invites do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_invite(User.t(), attrs :: map()) ::
 | 
					  @spec create_invite(User.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.new_invite())}
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
  def create_invite(%User{id: user_id, role: :admin}, attrs) do
 | 
					  def create_invite(%User{role: :admin} = user, attrs) do
 | 
				
			||||||
    token =
 | 
					    token =
 | 
				
			||||||
      :crypto.strong_rand_bytes(@invite_token_length)
 | 
					      :crypto.strong_rand_bytes(@invite_token_length)
 | 
				
			||||||
      |> Base.url_encode64()
 | 
					      |> Base.url_encode64()
 | 
				
			||||||
      |> binary_part(0, @invite_token_length)
 | 
					      |> binary_part(0, @invite_token_length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    attrs = attrs |> Map.merge(%{"user_id" => user_id, "token" => token})
 | 
					    Invite.create_changeset(user, token, attrs) |> Repo.insert()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    %Invite{} |> Invite.create_changeset(attrs) |> Repo.insert()
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -124,7 +121,7 @@ defmodule Cannery.Invites do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
 | 
					  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
  def update_invite(invite, attrs, %User{role: :admin}),
 | 
					  def update_invite(invite, attrs, %User{role: :admin}),
 | 
				
			||||||
    do: invite |> Invite.update_changeset(attrs) |> Repo.update()
 | 
					    do: invite |> Invite.update_changeset(attrs) |> Repo.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -141,7 +138,7 @@ defmodule Cannery.Invites do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_invite(Invite.t(), User.t()) ::
 | 
					  @spec delete_invite(Invite.t(), User.t()) ::
 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
  def delete_invite(invite, %User{role: :admin}), do: invite |> Repo.delete()
 | 
					  def delete_invite(invite, %User{role: :admin}), do: invite |> Repo.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -155,19 +152,4 @@ defmodule Cannery.Invites do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
 | 
					  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
 | 
				
			||||||
  def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!()
 | 
					  def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!()
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking invite changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_invite(invite)
 | 
					 | 
				
			||||||
      %Changeset{data: %Invite{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_invite(Invite.t() | Invite.new_invite()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
					 | 
				
			||||||
  @spec change_invite(Invite.t() | Invite.new_invite(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
					 | 
				
			||||||
  def change_invite(invite, attrs \\ %{}), do: invite |> Invite.update_changeset(attrs)
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,22 +36,24 @@ defmodule Cannery.Invites.Invite do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_invite :: %Invite{}
 | 
					  @type new_invite :: %Invite{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_invite())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_invite(), attrs :: map()) :: Changeset.t(new_invite())
 | 
					  @spec create_changeset(User.t(), token :: binary(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def create_changeset(invite, attrs) do
 | 
					  def create_changeset(%User{id: user_id}, token, attrs) do
 | 
				
			||||||
    invite
 | 
					    %Invite{}
 | 
				
			||||||
    |> cast(attrs, [:name, :token, :uses_left, :disabled_at, :user_id])
 | 
					    |> change(token: token, user_id: user_id)
 | 
				
			||||||
    |> validate_required([:name, :token, :user_id])
 | 
					 | 
				
			||||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc false
 | 
					 | 
				
			||||||
  @spec update_changeset(t() | new_invite(), attrs :: map()) :: Changeset.t(t() | new_invite())
 | 
					 | 
				
			||||||
  def update_changeset(invite, attrs) do
 | 
					 | 
				
			||||||
    invite
 | 
					 | 
				
			||||||
    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
					    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
				
			||||||
    |> validate_required([:name, :token, :user_id])
 | 
					    |> validate_required([:name, :token, :user_id])
 | 
				
			||||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
					    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc false
 | 
				
			||||||
 | 
					  @spec update_changeset(t() | new_invite(), attrs :: map()) :: changeset()
 | 
				
			||||||
 | 
					  def update_changeset(invite, attrs) do
 | 
				
			||||||
 | 
					    invite
 | 
				
			||||||
 | 
					    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
				
			||||||
 | 
					    |> validate_required([:name])
 | 
				
			||||||
 | 
					    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
  import Ecto.Query, warn: false
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
  import CanneryWeb.Gettext
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  alias Cannery.{Accounts.User, Repo, Tags.Tag}
 | 
					  alias Cannery.{Accounts.User, Repo, Tags.Tag}
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of tags.
 | 
					  Returns the list of tags.
 | 
				
			||||||
@@ -16,11 +15,38 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
      iex> list_tags(%User{id: 123})
 | 
					      iex> list_tags(%User{id: 123})
 | 
				
			||||||
      [%Tag{}, ...]
 | 
					      [%Tag{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_tags("cool", %User{id: 123})
 | 
				
			||||||
 | 
					      [%Tag{name: "my cool tag"}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_tags(User.t()) :: [Tag.t()]
 | 
					  @spec list_tags(User.t()) :: [Tag.t()]
 | 
				
			||||||
  def list_tags(%{id: user_id}),
 | 
					  @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)
 | 
					    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 """
 | 
					  @doc """
 | 
				
			||||||
  Gets a single tag.
 | 
					  Gets a single tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,9 +99,9 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_tag(attrs :: map(), User.t()) ::
 | 
					  @spec create_tag(attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, Tag.t()} | {:error, Changeset.t(Tag.new_tag())}
 | 
					          {:ok, Tag.t()} | {:error, Tag.changeset()}
 | 
				
			||||||
  def create_tag(attrs, %User{id: user_id}),
 | 
					  def create_tag(attrs, %User{} = user),
 | 
				
			||||||
    do: %Tag{} |> Tag.create_changeset(attrs |> Map.put("user_id", user_id)) |> Repo.insert()
 | 
					    do: %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Updates a tag.
 | 
					  Updates a tag.
 | 
				
			||||||
@@ -90,7 +116,7 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
 | 
					  @spec update_tag(Tag.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
          {:ok, Tag.t()} | {:error, Changeset.t(Tag.t())}
 | 
					          {:ok, Tag.t()} | {:error, Tag.changeset()}
 | 
				
			||||||
  def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}),
 | 
					  def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}),
 | 
				
			||||||
    do: tag |> Tag.update_changeset(attrs) |> Repo.update()
 | 
					    do: tag |> Tag.update_changeset(attrs) |> Repo.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,7 +132,7 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Changeset.t(Tag.t())}
 | 
					  @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()
 | 
					  def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -120,32 +146,4 @@ defmodule Cannery.Tags do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
 | 
					  @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!()
 | 
					  def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete!()
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking tag changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_tag(tag)
 | 
					 | 
				
			||||||
      %Changeset{data: %Tag{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_tag(Tag.t() | Tag.new_tag()) :: Changeset.t(Tag.t() | Tag.new_tag())
 | 
					 | 
				
			||||||
  @spec change_tag(Tag.t() | Tag.new_tag(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Tag.t() | Tag.new_tag())
 | 
					 | 
				
			||||||
  def change_tag(tag, attrs \\ %{}), do: Tag.update_changeset(tag, attrs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Get a random tag bg_color in `#ffffff` hex format
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> random_color()
 | 
					 | 
				
			||||||
      "#cc0066"
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec random_bg_color() :: <<_::7>>
 | 
					 | 
				
			||||||
  def random_bg_color do
 | 
					 | 
				
			||||||
    ["#cc0066", "#ff6699", "#6666ff", "#0066cc", "#00cc66", "#669900", "#ff9900", "#996633"]
 | 
					 | 
				
			||||||
    |> Enum.random()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,13 @@ defmodule Cannery.Tags.Tag do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Cannery.{Accounts.User, Tags.Tag}
 | 
					  alias Cannery.{Accounts.User, Tags.Tag}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :name,
 | 
				
			||||||
 | 
					             :bg_color,
 | 
				
			||||||
 | 
					             :text_color
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "tags" do
 | 
					  schema "tags" do
 | 
				
			||||||
@@ -33,20 +40,22 @@ defmodule Cannery.Tags.Tag do
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_tag() :: %Tag{}
 | 
					  @type new_tag() :: %Tag{}
 | 
				
			||||||
  @type id() :: UUID.t()
 | 
					  @type id() :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset() :: Changeset.t(t() | new_tag())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_tag(), attrs :: map()) :: Changeset.t(new_tag())
 | 
					  @spec create_changeset(new_tag(), User.t(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def create_changeset(tag, attrs) do
 | 
					  def create_changeset(tag, %User{id: user_id}, attrs) do
 | 
				
			||||||
    tag
 | 
					 | 
				
			||||||
    |> cast(attrs, [:name, :bg_color, :text_color, :user_id])
 | 
					 | 
				
			||||||
    |> validate_required([:name, :bg_color, :text_color, :user_id])
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc false
 | 
					 | 
				
			||||||
  @spec update_changeset(t() | new_tag(), attrs :: map()) :: Changeset.t(t() | new_tag())
 | 
					 | 
				
			||||||
  def update_changeset(tag, attrs) do
 | 
					 | 
				
			||||||
    tag
 | 
					    tag
 | 
				
			||||||
 | 
					    |> change(user_id: user_id)
 | 
				
			||||||
    |> cast(attrs, [:name, :bg_color, :text_color])
 | 
					    |> cast(attrs, [:name, :bg_color, :text_color])
 | 
				
			||||||
    |> validate_required([:name, :bg_color, :text_color, :user_id])
 | 
					    |> validate_required([:name, :bg_color, :text_color, :user_id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc false
 | 
				
			||||||
 | 
					  @spec update_changeset(t() | new_tag(), attrs :: map()) :: changeset()
 | 
				
			||||||
 | 
					  def update_changeset(tag, attrs) do
 | 
				
			||||||
 | 
					    tag
 | 
				
			||||||
 | 
					    |> cast(attrs, [:name, :bg_color, :text_color])
 | 
				
			||||||
 | 
					    |> validate_required([:name, :bg_color, :text_color])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,6 +47,7 @@ defmodule CanneryWeb do
 | 
				
			|||||||
      use Phoenix.LiveView,
 | 
					      use Phoenix.LiveView,
 | 
				
			||||||
        layout: {CanneryWeb.LayoutView, "live.html"}
 | 
					        layout: {CanneryWeb.LayoutView, "live.html"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      on_mount CanneryWeb.InitAssigns
 | 
				
			||||||
      unquote(view_helpers())
 | 
					      unquote(view_helpers())
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -95,7 +96,7 @@ defmodule CanneryWeb do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
 | 
					      # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
 | 
				
			||||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      import Phoenix.LiveView.Helpers
 | 
					      import Phoenix.Component
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Import basic rendering functionality (render, render_layout, etc)
 | 
					      # Import basic rendering functionality (render, render_layout, etc)
 | 
				
			||||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
@@ -103,6 +104,7 @@ defmodule CanneryWeb do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      import CanneryWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers}
 | 
					      import CanneryWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers}
 | 
				
			||||||
 | 
					      alias CanneryWeb.Endpoint
 | 
				
			||||||
      alias CanneryWeb.Router.Helpers, as: Routes
 | 
					      alias CanneryWeb.Router.Helpers, as: Routes
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_component
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
  alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup}
 | 
					  alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup}
 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.{JS, Socket}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  @spec update(
 | 
					  @spec update(
 | 
				
			||||||
@@ -16,9 +16,10 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          Socket.t()
 | 
					          Socket.t()
 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{ammo_group: _ammo_group, current_user: _current_user} = assigns, socket) do
 | 
					  def update(%{ammo_group: ammo_group, current_user: current_user} = assigns, socket) do
 | 
				
			||||||
    changeset =
 | 
					    changeset =
 | 
				
			||||||
      %ShotGroup{date: NaiveDateTime.utc_now(), count: 1} |> ActivityLog.change_shot_group()
 | 
					      %ShotGroup{date: NaiveDateTime.utc_now(), count: 1}
 | 
				
			||||||
 | 
					      |> ShotGroup.create_changeset(current_user, ammo_group, %{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)}
 | 
					    {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -27,21 +28,13 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
 | 
				
			|||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "validate",
 | 
					        "validate",
 | 
				
			||||||
        %{"shot_group" => shot_group_params},
 | 
					        %{"shot_group" => shot_group_params},
 | 
				
			||||||
        %{
 | 
					        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
				
			||||||
          assigns: %{
 | 
					 | 
				
			||||||
            ammo_group: %AmmoGroup{id: ammo_group_id} = ammo_group,
 | 
					 | 
				
			||||||
            current_user: %User{id: user_id}
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    shot_group_params =
 | 
					    params = shot_group_params |> process_params(ammo_group)
 | 
				
			||||||
      shot_group_params
 | 
					 | 
				
			||||||
      |> process_params(ammo_group)
 | 
					 | 
				
			||||||
      |> Map.merge(%{"ammo_group_id" => ammo_group_id, "user_id" => user_id})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    changeset =
 | 
					    changeset =
 | 
				
			||||||
      %ShotGroup{}
 | 
					      %ShotGroup{}
 | 
				
			||||||
      |> ActivityLog.change_shot_group(shot_group_params)
 | 
					      |> ShotGroup.create_changeset(current_user, ammo_group, params)
 | 
				
			||||||
      |> Map.put(:action, :validate)
 | 
					      |> Map.put(:action, :validate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, changeset)}
 | 
					    {:noreply, socket |> assign(:changeset, changeset)}
 | 
				
			||||||
@@ -51,22 +44,17 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do
 | 
				
			|||||||
        "save",
 | 
					        "save",
 | 
				
			||||||
        %{"shot_group" => shot_group_params},
 | 
					        %{"shot_group" => shot_group_params},
 | 
				
			||||||
        %{
 | 
					        %{
 | 
				
			||||||
          assigns: %{
 | 
					          assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}
 | 
				
			||||||
            ammo_group: %{id: ammo_group_id} = ammo_group,
 | 
					 | 
				
			||||||
            current_user: %{id: user_id} = current_user,
 | 
					 | 
				
			||||||
            return_to: return_to
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } = socket
 | 
					        } = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      shot_group_params
 | 
					      shot_group_params
 | 
				
			||||||
      |> process_params(ammo_group)
 | 
					      |> process_params(ammo_group)
 | 
				
			||||||
      |> Map.merge(%{"ammo_group_id" => ammo_group_id, "user_id" => user_id})
 | 
					 | 
				
			||||||
      |> ActivityLog.create_shot_group(current_user, ammo_group)
 | 
					      |> ActivityLog.create_shot_group(current_user, ammo_group)
 | 
				
			||||||
      |> case do
 | 
					      |> case do
 | 
				
			||||||
        {:ok, _shot_group} ->
 | 
					        {:ok, _shot_group} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "Shots recorded successfully")
 | 
					          prompt = dgettext("prompts", "Shots recorded successfully")
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Ecto.Changeset{} = changeset} ->
 | 
					        {:error, %Ecto.Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="shot-group-form"
 | 
					    id="shot-group-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"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
@@ -22,9 +22,16 @@
 | 
				
			|||||||
    <%= number_input(f, :ammo_left,
 | 
					    <%= number_input(f, :ammo_left,
 | 
				
			||||||
      min: 0,
 | 
					      min: 0,
 | 
				
			||||||
      max: @ammo_group.count - 1,
 | 
					      max: @ammo_group.count - 1,
 | 
				
			||||||
      placeholder: 0,
 | 
					      placeholder: gettext("Rounds left"),
 | 
				
			||||||
      class: "input input-primary col-span-2"
 | 
					      class: "input input-primary"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      type="button"
 | 
				
			||||||
 | 
					      class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
 | 
					      phx-click={JS.dispatch("cannery:set-zero", to: "#shot-group-form_ammo_left")}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= gettext("Used up!") %>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
    <%= error_tag(f, :ammo_left, "col-span-3") %>
 | 
					    <%= error_tag(f, :ammo_left, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
 | 
					    <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
@@ -35,7 +42,7 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :notes, "col-span-3") %>
 | 
					    <%= error_tag(f, :notes, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :date, gettext("Date (UTC)"), class: "title text-lg text-primary-600") %>
 | 
					    <%= label(f, :date, gettext("Date"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= date_input(f, :date,
 | 
					    <%= date_input(f, :date,
 | 
				
			||||||
      class: "input input-primary col-span-2",
 | 
					      class: "input input-primary col-span-2",
 | 
				
			||||||
      phx_update: "ignore",
 | 
					      phx_update: "ignore",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,21 @@ defmodule CanneryWeb.Components.AmmoGroupCard do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
  alias Cannery.Repo
 | 
					  alias Cannery.{Ammo, Ammo.AmmoGroup, Repo}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def ammo_group_card(assigns) do
 | 
					  attr :ammo_group, AmmoGroup, required: true
 | 
				
			||||||
    assigns = assigns |> assign(:ammo_group, assigns.ammo_group |> Repo.preload(:ammo_type))
 | 
					  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"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
@@ -17,19 +27,25 @@ defmodule CanneryWeb.Components.AmmoGroupCard do
 | 
				
			|||||||
            border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
					            border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
            transition-all duration-300 ease-in-out"
 | 
					            transition-all duration-300 ease-in-out"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <%= live_redirect to: Routes.ammo_group_show_path(Endpoint, :show, @ammo_group),
 | 
					      <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link">
 | 
				
			||||||
                    class: "mb-2 link" do %>
 | 
					 | 
				
			||||||
        <h1 class="title text-xl title-primary-500">
 | 
					        <h1 class="title text-xl title-primary-500">
 | 
				
			||||||
          <%= @ammo_group.ammo_type.name %>
 | 
					          <%= @ammo_group.ammo_type.name %>
 | 
				
			||||||
        </h1>
 | 
					        </h1>
 | 
				
			||||||
      <% end %>
 | 
					      </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="flex flex-col justify-center items-center">
 | 
					      <div class="flex flex-col justify-center items-center">
 | 
				
			||||||
        <span class="rounded-lg title text-lg">
 | 
					        <span class="rounded-lg title text-lg">
 | 
				
			||||||
          <%= gettext("Count:") %>
 | 
					          <%= gettext("Count:") %>
 | 
				
			||||||
          <%= @ammo_group.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 do %>
 | 
				
			||||||
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					            <%= gettext("Original Count:") %>
 | 
				
			||||||
 | 
					            <%= @ammo_group |> Ammo.get_original_count() %>
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= if @ammo_group.notes do %>
 | 
					        <%= if @ammo_group.notes do %>
 | 
				
			||||||
          <span class="rounded-lg title text-lg">
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
            <%= gettext("Notes:") %>
 | 
					            <%= gettext("Notes:") %>
 | 
				
			||||||
@@ -37,6 +53,18 @@ defmodule CanneryWeb.Components.AmmoGroupCard do
 | 
				
			|||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					          <%= gettext("Purchased on:") %>
 | 
				
			||||||
 | 
					          <%= @ammo_group.purchased_on |> display_date() %>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= if @ammo_group |> Ammo.get_last_used_shot_group() do %>
 | 
				
			||||||
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					            <%= gettext("Last used on:") %>
 | 
				
			||||||
 | 
					            <%= @ammo_group |> Ammo.get_last_used_shot_group() |> Map.get(:date) |> display_date() %>
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= if @ammo_group.price_paid do %>
 | 
					        <%= if @ammo_group.price_paid do %>
 | 
				
			||||||
          <span class="rounded-lg title text-lg">
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
            <%= gettext("Price paid:") %>
 | 
					            <%= gettext("Price paid:") %>
 | 
				
			||||||
@@ -44,6 +72,26 @@ defmodule CanneryWeb.Components.AmmoGroupCard do
 | 
				
			|||||||
              amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
 | 
					              amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
 | 
				
			||||||
            ) %>
 | 
					            ) %>
 | 
				
			||||||
          </span>
 | 
					          </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 %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= if @show_container and @ammo_group.container do %>
 | 
				
			||||||
 | 
					          <span 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>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										234
									
								
								lib/cannery_web/components/ammo_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								lib/cannery_web/components/ammo_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,234 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.Components.AmmoGroupTableComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  A component that displays a list of ammo groups
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
 | 
					  alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Repo}
 | 
				
			||||||
 | 
					  alias Ecto.UUID
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.{Rendered, Socket}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{
 | 
				
			||||||
 | 
					            required(:id) => UUID.t(),
 | 
				
			||||||
 | 
					            required(:current_user) => User.t(),
 | 
				
			||||||
 | 
					            required(:ammo_groups) => [AmmoGroup.t()],
 | 
				
			||||||
 | 
					            optional(:ammo_type) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(:range) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(:container) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(:actions) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(any()) => any()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{id: _id, ammo_groups: _ammo_group, current_user: _current_user} = assigns, socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> assign_new(:ammo_type, fn -> [] end)
 | 
				
			||||||
 | 
					      |> assign_new(:range, fn -> [] end)
 | 
				
			||||||
 | 
					      |> assign_new(:container, fn -> [] end)
 | 
				
			||||||
 | 
					      |> assign_new(:actions, fn -> [] end)
 | 
				
			||||||
 | 
					      |> display_ammo_groups()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_groups(
 | 
				
			||||||
 | 
					         %{
 | 
				
			||||||
 | 
					           assigns: %{
 | 
				
			||||||
 | 
					             ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					             current_user: current_user,
 | 
				
			||||||
 | 
					             ammo_type: ammo_type,
 | 
				
			||||||
 | 
					             range: range,
 | 
				
			||||||
 | 
					             container: container,
 | 
				
			||||||
 | 
					             actions: actions
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					         } = 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},
 | 
				
			||||||
 | 
					      %{label: gettext("% left"), key: :remaining},
 | 
				
			||||||
 | 
					      %{label: gettext("Notes"), key: :notes}
 | 
				
			||||||
 | 
					      | columns
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    columns =
 | 
				
			||||||
 | 
					      if ammo_type == [] do
 | 
				
			||||||
 | 
					        columns
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        [%{label: gettext("Ammo type"), key: :ammo_type} | columns]
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extra_data = %{
 | 
				
			||||||
 | 
					      current_user: current_user,
 | 
				
			||||||
 | 
					      ammo_type: ammo_type,
 | 
				
			||||||
 | 
					      columns: columns,
 | 
				
			||||||
 | 
					      container: container,
 | 
				
			||||||
 | 
					      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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(columns: columns, rows: rows)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def render(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div id={@id} class="w-full">
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.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)}
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_value_for_key(atom(), AmmoGroup.t(), additional_data :: map()) ::
 | 
				
			||||||
 | 
					          any() | {any(), Rendered.t()}
 | 
				
			||||||
 | 
					  defp get_value_for_key(
 | 
				
			||||||
 | 
					         :ammo_type,
 | 
				
			||||||
 | 
					         %{ammo_type: %{name: ammo_type_name} = ammo_type},
 | 
				
			||||||
 | 
					         %{ammo_type: ammo_type_block}
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    assigns = %{ammo_type: ammo_type, ammo_type_block: ammo_type_block}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {ammo_type_name,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= render_slot(@ammo_type_block, @ammo_type) %>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:price_paid, %{price_paid: nil}, _additional_data), do: {"", nil}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:price_paid, %{price_paid: price_paid}, _additional_data),
 | 
				
			||||||
 | 
					    do: gettext("$%{amount}", amount: price_paid |> :erlang.float_to_binary(decimals: 2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on}, _additional_data) do
 | 
				
			||||||
 | 
					    assigns = %{purchased_on: purchased_on}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {purchased_on,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= @purchased_on |> display_date() %>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assigns = %{last_shot_group_date: last_shot_group_date}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {last_shot_group_date,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= if @last_shot_group_date do %>
 | 
				
			||||||
 | 
					       <%= @last_shot_group_date |> display_date() %>
 | 
				
			||||||
 | 
					     <% else %>
 | 
				
			||||||
 | 
					       <%= gettext("Never used") %>
 | 
				
			||||||
 | 
					     <% end %>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:range, %{staged: staged} = ammo_group, %{range: range}) do
 | 
				
			||||||
 | 
					    assigns = %{range: range, ammo_group: ammo_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {staged,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= render_slot(@range, @ammo_group) %>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  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(:actions, ammo_group, %{actions: actions}) do
 | 
				
			||||||
 | 
					    assigns = %{actions: actions, ammo_group: ammo_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <%= render_slot(@actions, @ammo_group) %>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:container, %{container: nil}, _additional_data), do: {nil, nil}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(
 | 
				
			||||||
 | 
					         :container,
 | 
				
			||||||
 | 
					         %{container: %{name: container_name}} = ammo_group,
 | 
				
			||||||
 | 
					         %{container: container}
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    assigns = %{container: container, ammo_group: ammo_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {container_name,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= render_slot(@container, @ammo_group) %>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:original_count, ammo_group, _additional_data),
 | 
				
			||||||
 | 
					    do: ammo_group |> Ammo.get_original_count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data),
 | 
				
			||||||
 | 
					    do: gettext("No cost information")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:cpr, ammo_group, _additional_data) do
 | 
				
			||||||
 | 
					    gettext("$%{amount}",
 | 
				
			||||||
 | 
					      amount: ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:count, %{count: count}, _additional_data),
 | 
				
			||||||
 | 
					    do: if(count == 0, do: gettext("Empty"), else: count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(key, ammo_group, _additional_data), do: ammo_group |> Map.get(key)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										209
									
								
								lib/cannery_web/components/ammo_type_table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								lib/cannery_web/components/ammo_type_table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,209 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.Components.AmmoTypeTableComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  A component that displays a list of ammo type
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
 | 
					  alias Cannery.{Accounts.User, Ammo, Ammo.AmmoType}
 | 
				
			||||||
 | 
					  alias Ecto.UUID
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.{Rendered, Socket}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{
 | 
				
			||||||
 | 
					            required(:id) => UUID.t(),
 | 
				
			||||||
 | 
					            required(:current_user) => User.t(),
 | 
				
			||||||
 | 
					            optional(:show_used) => boolean(),
 | 
				
			||||||
 | 
					            optional(:ammo_types) => [AmmoType.t()],
 | 
				
			||||||
 | 
					            optional(:actions) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(any()) => any()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{id: _id, ammo_types: _ammo_types, current_user: _current_user} = assigns, socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> assign_new(:show_used, fn -> false end)
 | 
				
			||||||
 | 
					      |> assign_new(:actions, fn -> [] end)
 | 
				
			||||||
 | 
					      |> display_ammo_types()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_types(
 | 
				
			||||||
 | 
					         %{
 | 
				
			||||||
 | 
					           assigns: %{
 | 
				
			||||||
 | 
					             ammo_types: ammo_types,
 | 
				
			||||||
 | 
					             current_user: current_user,
 | 
				
			||||||
 | 
					             show_used: show_used,
 | 
				
			||||||
 | 
					             actions: actions
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					         } = socket
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    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: 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}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					      |> Enum.filter(fn %{key: key, type: type} ->
 | 
				
			||||||
 | 
					        # remove columns if all values match defaults
 | 
				
			||||||
 | 
					        default_value = if type == :boolean, do: false, else: nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ammo_types
 | 
				
			||||||
 | 
					        |> Enum.any?(fn ammo_type ->
 | 
				
			||||||
 | 
					          not (ammo_type |> Map.get(key) == default_value)
 | 
				
			||||||
 | 
					        end)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      |> Kernel.++([
 | 
				
			||||||
 | 
					        %{label: gettext("Rounds"), key: :round_count, type: :round_count}
 | 
				
			||||||
 | 
					      ])
 | 
				
			||||||
 | 
					      |> Kernel.++(
 | 
				
			||||||
 | 
					        if show_used do
 | 
				
			||||||
 | 
					          [
 | 
				
			||||||
 | 
					            %{
 | 
				
			||||||
 | 
					              label: gettext("Used rounds"),
 | 
				
			||||||
 | 
					              key: :used_round_count,
 | 
				
			||||||
 | 
					              type: :used_round_count
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            %{
 | 
				
			||||||
 | 
					              label: gettext("Total ever rounds"),
 | 
				
			||||||
 | 
					              key: :historical_round_count,
 | 
				
			||||||
 | 
					              type: :historical_round_count
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          []
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      |> Kernel.++([%{label: gettext("Packs"), key: :ammo_count, type: :ammo_count}])
 | 
				
			||||||
 | 
					      |> Kernel.++(
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          []
 | 
				
			||||||
 | 
					        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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows =
 | 
				
			||||||
 | 
					      ammo_types
 | 
				
			||||||
 | 
					      |> Enum.map(fn ammo_type ->
 | 
				
			||||||
 | 
					        ammo_type |> get_ammo_type_values(columns, extra_data)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(columns: columns, rows: rows)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def render(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div id={@id} class="w-full">
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
 | 
					        id={"table-#{@id}"}
 | 
				
			||||||
 | 
					        columns={@columns}
 | 
				
			||||||
 | 
					        rows={@rows}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_values(ammo_type, columns, extra_data) do
 | 
				
			||||||
 | 
					    columns
 | 
				
			||||||
 | 
					    |> Map.new(fn %{key: key, type: type} ->
 | 
				
			||||||
 | 
					      {key, get_ammo_type_value(type, key, ammo_type, extra_data)}
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_value(:boolean, key, ammo_type, _other_data),
 | 
				
			||||||
 | 
					    do: ammo_type |> Map.get(key) |> humanize()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_value(:round_count, _key, ammo_type, %{current_user: current_user}),
 | 
				
			||||||
 | 
					    do: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_value(: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(: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(: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(: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(: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(: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))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_value(:name, _key, ammo_type, _other_data) do
 | 
				
			||||||
 | 
					    assigns = %{ammo_type: ammo_type}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <.link
 | 
				
			||||||
 | 
					      navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_type)}
 | 
				
			||||||
 | 
					      class="link"
 | 
				
			||||||
 | 
					      data-qa={"view-name-#{@ammo_type.id}"}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= @ammo_type.name %>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_ammo_type_value(:actions, _key, ammo_type, %{actions: actions}) do
 | 
				
			||||||
 | 
					    assigns = %{actions: actions, ammo_type: ammo_type}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <%= render_slot(@actions, @ammo_type) %>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -5,26 +5,34 @@ defmodule CanneryWeb.Components.ContainerCard do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
  import CanneryWeb.Components.TagCard
 | 
					  import CanneryWeb.Components.TagCard
 | 
				
			||||||
  alias Cannery.{Containers, Repo}
 | 
					  alias Cannery.{Containers, Containers.Container, Repo}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  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
 | 
					  def container_card(%{container: container} = assigns) do
 | 
				
			||||||
    assigns = assigns |> Map.put(:container, container |> Repo.preload([:tags, :ammo_groups]))
 | 
					    assigns =
 | 
				
			||||||
 | 
					      assigns
 | 
				
			||||||
 | 
					      |> assign(container: container |> Repo.preload([:tags, :ammo_groups]))
 | 
				
			||||||
 | 
					      |> assign_new(:tag_actions, fn -> [] end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      id={"container-#{@container.id}"}
 | 
					      id={"container-#{@container.id}"}
 | 
				
			||||||
      class="overflow-hidden max-w-full mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
					      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
 | 
					        border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
        transition-all duration-300 ease-in-out"
 | 
					        transition-all duration-300 ease-in-out"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
 | 
					      <div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
 | 
				
			||||||
        <%= live_redirect to: Routes.container_show_path(Endpoint, :show, @container),
 | 
					        <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
 | 
				
			||||||
                      class: "link" do %>
 | 
					 | 
				
			||||||
          <h1 class="px-4 py-2 rounded-lg title text-xl">
 | 
					          <h1 class="px-4 py-2 rounded-lg title text-xl">
 | 
				
			||||||
            <%= @container.name %>
 | 
					            <%= @container.name %>
 | 
				
			||||||
          </h1>
 | 
					          </h1>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= if @container.desc do %>
 | 
					        <%= if @container.desc do %>
 | 
				
			||||||
          <span class="rounded-lg title text-lg">
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
@@ -45,7 +53,12 @@ defmodule CanneryWeb.Components.ContainerCard do
 | 
				
			|||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= if @container.ammo_groups do %>
 | 
					        <%= 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">
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
            <%= gettext("Rounds:") %>
 | 
					            <%= gettext("Rounds:") %>
 | 
				
			||||||
            <%= @container |> Containers.get_container_rounds!() %>
 | 
					            <%= @container |> Containers.get_container_rounds!() %>
 | 
				
			||||||
@@ -59,9 +72,7 @@ defmodule CanneryWeb.Components.ContainerCard do
 | 
				
			|||||||
            <% end %>
 | 
					            <% end %>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <%= if assigns |> Map.has_key?(:tag_actions) do %>
 | 
					          <%= render_slot(@tag_actions) %>
 | 
				
			||||||
            <%= render_slot(@tag_actions) %>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										158
									
								
								lib/cannery_web/components/container_table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								lib/cannery_web/components/container_table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.Components.ContainerTableComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  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 Ecto.UUID
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.{Rendered, Socket}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{
 | 
				
			||||||
 | 
					            required(:id) => UUID.t(),
 | 
				
			||||||
 | 
					            required(:current_user) => User.t(),
 | 
				
			||||||
 | 
					            optional(:containers) => [Container.t()],
 | 
				
			||||||
 | 
					            optional(:tag_actions) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(:actions) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(any()) => any()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{id: _id, containers: _containers, current_user: _current_user} = assigns, socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> assign_new(:tag_actions, fn -> [] end)
 | 
				
			||||||
 | 
					      |> assign_new(:actions, fn -> [] end)
 | 
				
			||||||
 | 
					      |> display_containers()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_containers(
 | 
				
			||||||
 | 
					         %{
 | 
				
			||||||
 | 
					           assigns: %{
 | 
				
			||||||
 | 
					             containers: containers,
 | 
				
			||||||
 | 
					             current_user: current_user,
 | 
				
			||||||
 | 
					             tag_actions: tag_actions,
 | 
				
			||||||
 | 
					             actions: actions
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					         } = socket
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    columns =
 | 
				
			||||||
 | 
					      [
 | 
				
			||||||
 | 
					        %{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}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					      |> Enum.filter(fn %{key: key, type: type} ->
 | 
				
			||||||
 | 
					        # remove columns if all values match defaults
 | 
				
			||||||
 | 
					        default_value =
 | 
				
			||||||
 | 
					          case type do
 | 
				
			||||||
 | 
					            :boolean -> false
 | 
				
			||||||
 | 
					            _other_type -> nil
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        containers
 | 
				
			||||||
 | 
					        |> Enum.any?(fn container ->
 | 
				
			||||||
 | 
					          type in [:tags, :actions] or not (container |> Map.get(key) == default_value)
 | 
				
			||||||
 | 
					        end)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extra_data = %{
 | 
				
			||||||
 | 
					      current_user: current_user,
 | 
				
			||||||
 | 
					      tag_actions: tag_actions,
 | 
				
			||||||
 | 
					      actions: actions
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows =
 | 
				
			||||||
 | 
					      containers
 | 
				
			||||||
 | 
					      |> Enum.map(fn container ->
 | 
				
			||||||
 | 
					        container |> get_row_data_for_container(columns, extra_data)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      columns: columns,
 | 
				
			||||||
 | 
					      rows: rows
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def render(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div id={@id} class="w-full">
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
 | 
					        id={"table-#{@id}"}
 | 
				
			||||||
 | 
					        columns={@columns}
 | 
				
			||||||
 | 
					        rows={@rows}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_value_for_key(atom(), Container.t(), extra_data :: map) :: any()
 | 
				
			||||||
 | 
					  defp get_value_for_key(:name, %{id: id, name: container_name}, _extra_data) do
 | 
				
			||||||
 | 
					    assigns = %{id: id, container_name: container_name}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {container_name,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
 | 
					       <.link navigate={Routes.container_show_path(Endpoint, :show, @id)} class="link">
 | 
				
			||||||
 | 
					         <%= @container_name %>
 | 
				
			||||||
 | 
					       </.link>
 | 
				
			||||||
 | 
					     </div>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:packs, container, _extra_data) do
 | 
				
			||||||
 | 
					    container |> Containers.get_container_ammo_group_count!()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:rounds, container, _extra_data) do
 | 
				
			||||||
 | 
					    container |> Containers.get_container_rounds!()
 | 
				
			||||||
 | 
					  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),
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
 | 
					       <%= unless @container.tags |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					         <%= for tag <- @container.tags do %>
 | 
				
			||||||
 | 
					           <TagCard.simple_tag_card tag={tag} />
 | 
				
			||||||
 | 
					         <% end %>
 | 
				
			||||||
 | 
					       <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       <%= render_slot(@tag_actions, @container) %>
 | 
				
			||||||
 | 
					     </div>
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:actions, container, %{actions: actions}) do
 | 
				
			||||||
 | 
					    assigns = %{actions: actions, container: container}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <%= render_slot(@actions, @container) %>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(key, container, _extra_data), do: container |> Map.get(key)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -4,13 +4,23 @@ defmodule CanneryWeb.Components.InviteCard do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
 | 
					  alias Cannery.Invites.Invite
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :invite, Invite, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block)
 | 
				
			||||||
 | 
					  slot(:code_actions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def invite_card(assigns) do
 | 
					  def invite_card(assigns) do
 | 
				
			||||||
 | 
					    assigns = assigns |> assign_new(:code_actions, fn -> [] end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
					    <div
 | 
				
			||||||
 | 
					      id={"invite-#{@invite.id}"}
 | 
				
			||||||
 | 
					      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
 | 
					      border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
      transition-all duration-300 ease-in-out">
 | 
					      transition-all duration-300 ease-in-out"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <h1 class="title text-xl">
 | 
					      <h1 class="title text-xl">
 | 
				
			||||||
        <%= @invite.name %>
 | 
					        <%= @invite.name %>
 | 
				
			||||||
      </h1>
 | 
					      </h1>
 | 
				
			||||||
@@ -30,13 +40,9 @@ defmodule CanneryWeb.Components.InviteCard do
 | 
				
			|||||||
        <code
 | 
					        <code
 | 
				
			||||||
          id={"code-#{@invite.id}"}
 | 
					          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"
 | 
					          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) %>
 | 
					        ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
 | 
				
			||||||
        </code>
 | 
					        <%= render_slot(@code_actions) %>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <%= if @code_actions do %>
 | 
					 | 
				
			||||||
          <%= render_slot(@code_actions) %>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= if @inner_block do %>
 | 
					      <%= if @inner_block do %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
          assigns,
 | 
					          assigns,
 | 
				
			||||||
        socket
 | 
					        socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    changeset = Ammo.change_ammo_group(ammo_group)
 | 
					    changeset = ammo_group |> AmmoGroup.update_changeset(%{}, current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    containers =
 | 
					    containers =
 | 
				
			||||||
      Containers.list_containers(current_user)
 | 
					      Containers.list_containers(current_user)
 | 
				
			||||||
@@ -52,7 +52,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
        {:ok, _ammo_group} ->
 | 
					        {:ok, _ammo_group} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "Ammo moved to %{name} successfully", name: container_name)
 | 
					          prompt = dgettext("prompts", "Ammo moved to %{name} successfully", name: container_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Ecto.Changeset{} = changeset} ->
 | 
					        {:error, %Ecto.Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
@@ -77,7 +77,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="w-full flex flex-col space-y-8 justify-center items-center">
 | 
					    <div class="w-full flex flex-col space-y-8 justify-center items-center">
 | 
				
			||||||
      <h2 class="mb-8 text-center title text-xl text-primary-600">
 | 
					      <h2 class="mb-8 text-center title text-xl text-primary-600">
 | 
				
			||||||
        <%= gettext("Move ammo") %>
 | 
					        <%= dgettext("actions", "Move ammo") %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= if @containers |> Enum.empty?() do %>
 | 
					      <%= if @containers |> Enum.empty?() do %>
 | 
				
			||||||
@@ -86,10 +86,9 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
          <%= display_emoji("😔") %>
 | 
					          <%= display_emoji("😔") %>
 | 
				
			||||||
        </h2>
 | 
					        </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= live_patch(dgettext("actions", "Add another container!"),
 | 
					        <.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
          to: Routes.container_index_path(Endpoint, :new),
 | 
					          <%= dgettext("actions", "Add another container!") %>
 | 
				
			||||||
          class: "btn btn-primary"
 | 
					        </.link>
 | 
				
			||||||
        ) %>
 | 
					 | 
				
			||||||
      <% else %>
 | 
					      <% else %>
 | 
				
			||||||
        <.live_component
 | 
					        <.live_component
 | 
				
			||||||
          module={CanneryWeb.Components.TableComponent}
 | 
					          module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
@@ -107,7 +106,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
    containers
 | 
					    containers
 | 
				
			||||||
    |> Enum.map(fn container ->
 | 
					    |> Enum.map(fn container ->
 | 
				
			||||||
      columns
 | 
					      columns
 | 
				
			||||||
      |> Enum.into(%{}, fn %{key: key} -> {key, get_row_value_by_key(key, container, assigns)} end)
 | 
					      |> Map.new(fn %{key: key} -> {key, get_row_value_by_key(key, container, assigns)} end)
 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,7 +121,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
        class="btn btn-primary"
 | 
					        class="btn btn-primary"
 | 
				
			||||||
        phx-click="move"
 | 
					        phx-click="move"
 | 
				
			||||||
        phx-target={@myself}
 | 
					        phx-target={@myself}
 | 
				
			||||||
        phx-value-container_id={container.id}
 | 
					        phx-value-container_id={@container.id}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <%= dgettext("actions", "Select") %>
 | 
					        <%= dgettext("actions", "Select") %>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										117
									
								
								lib/cannery_web/components/shot_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								lib/cannery_web/components/shot_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.Components.ShotGroupTableComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  A component that displays a list of shot groups
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
 | 
					  alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo}
 | 
				
			||||||
 | 
					  alias Ecto.UUID
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.{Rendered, Socket}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{
 | 
				
			||||||
 | 
					            required(:id) => UUID.t(),
 | 
				
			||||||
 | 
					            required(:current_user) => User.t(),
 | 
				
			||||||
 | 
					            optional(:shot_groups) => [ShotGroup.t()],
 | 
				
			||||||
 | 
					            optional(:actions) => Rendered.t(),
 | 
				
			||||||
 | 
					            optional(any()) => any()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{id: _id, shot_groups: _shot_groups, current_user: _current_user} = assigns, socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> assign_new(:actions, fn -> [] end)
 | 
				
			||||||
 | 
					      |> display_shot_groups()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_shot_groups(
 | 
				
			||||||
 | 
					         %{
 | 
				
			||||||
 | 
					           assigns: %{
 | 
				
			||||||
 | 
					             shot_groups: shot_groups,
 | 
				
			||||||
 | 
					             current_user: current_user,
 | 
				
			||||||
 | 
					             actions: actions
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					         } = socket
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    columns = [
 | 
				
			||||||
 | 
					      %{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}
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extra_data = %{current_user: current_user, actions: actions}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows =
 | 
				
			||||||
 | 
					      shot_groups
 | 
				
			||||||
 | 
					      |> Enum.map(fn shot_group ->
 | 
				
			||||||
 | 
					        shot_group |> get_row_data_for_shot_group(columns, extra_data)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      columns: columns,
 | 
				
			||||||
 | 
					      rows: rows
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def render(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div id={@id} class="w-full">
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
 | 
					        id={"table-#{@id}"}
 | 
				
			||||||
 | 
					        columns={@columns}
 | 
				
			||||||
 | 
					        rows={@rows}
 | 
				
			||||||
 | 
					        initial_key={:date}
 | 
				
			||||||
 | 
					        initial_sort_mode={:desc}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name_block = ~H"""
 | 
				
			||||||
 | 
					    <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="link">
 | 
				
			||||||
 | 
					      <%= @ammo_type_name %>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {ammo_type_name, name_block}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_row_value(:date, %{date: date}, _extra_data), do: date |> display_date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_row_value(:actions, shot_group, %{actions: actions}) do
 | 
				
			||||||
 | 
					    assigns = %{actions: actions, shot_group: shot_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <%= render_slot(@actions, @shot_group) %>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_row_value(key, shot_group, _extra_data), do: shot_group |> Map.get(key)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -6,7 +6,7 @@ defmodule CanneryWeb.Components.TableComponent do
 | 
				
			|||||||
    - `:columns`: An array of maps containing the following keys
 | 
					    - `:columns`: An array of maps containing the following keys
 | 
				
			||||||
      - `:label`: A gettext'd or otherwise user-facing string label for the
 | 
					      - `:label`: A gettext'd or otherwise user-facing string label for the
 | 
				
			||||||
        column. Can be nil
 | 
					        column. Can be nil
 | 
				
			||||||
      - `:key`: A string key used for sorting
 | 
					      - `:key`: An atom key used for sorting
 | 
				
			||||||
      - `:class`: Extra classes to be applied to the column element, if desired.
 | 
					      - `:class`: Extra classes to be applied to the column element, if desired.
 | 
				
			||||||
        Optional
 | 
					        Optional
 | 
				
			||||||
      - `:sortable`: If false, will prevent the user from sorting with it.
 | 
					      - `:sortable`: If false, will prevent the user from sorting with it.
 | 
				
			||||||
@@ -21,6 +21,7 @@ defmodule CanneryWeb.Components.TableComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_component
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					  require Integer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  @spec update(
 | 
					  @spec update(
 | 
				
			||||||
@@ -28,26 +29,50 @@ defmodule CanneryWeb.Components.TableComponent do
 | 
				
			|||||||
            required(:columns) =>
 | 
					            required(:columns) =>
 | 
				
			||||||
              list(%{
 | 
					              list(%{
 | 
				
			||||||
                required(:label) => String.t() | nil,
 | 
					                required(:label) => String.t() | nil,
 | 
				
			||||||
                required(:key) => String.t() | nil,
 | 
					                required(:key) => atom() | nil,
 | 
				
			||||||
                optional(:class) => String.t(),
 | 
					                optional(:class) => String.t(),
 | 
				
			||||||
 | 
					                optional(:row_class) => String.t(),
 | 
				
			||||||
 | 
					                optional(:alternate_row_class) => String.t(),
 | 
				
			||||||
                optional(:sortable) => false
 | 
					                optional(:sortable) => false
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
            required(:rows) =>
 | 
					            required(:rows) =>
 | 
				
			||||||
              list(%{
 | 
					              list(%{
 | 
				
			||||||
                (key :: String.t()) => any() | {custom_sort_value :: String.t(), value :: any()}
 | 
					                (key :: atom()) => any() | {custom_sort_value :: String.t(), value :: any()}
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
 | 
					            optional(:inital_key) => atom(),
 | 
				
			||||||
 | 
					            optional(:initial_sort_mode) => atom(),
 | 
				
			||||||
            optional(any()) => any()
 | 
					            optional(any()) => any()
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          Socket.t()
 | 
					          Socket.t()
 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{columns: columns, rows: rows} = assigns, socket) do
 | 
					  def update(%{columns: columns, rows: rows} = assigns, socket) do
 | 
				
			||||||
    initial_key = columns |> List.first() |> Map.get(:key)
 | 
					    initial_key =
 | 
				
			||||||
    rows = rows |> Enum.sort_by(fn row -> row |> Map.get(initial_key) end, :asc)
 | 
					      if assigns |> Map.has_key?(:initial_key) do
 | 
				
			||||||
 | 
					        assigns.initial_key
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        columns |> List.first(%{}) |> Map.get(:key)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial_sort_mode =
 | 
				
			||||||
 | 
					      if assigns |> Map.has_key?(:initial_sort_mode) do
 | 
				
			||||||
 | 
					        assigns.initial_sort_mode
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        :asc
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      socket
 | 
					      socket
 | 
				
			||||||
      |> assign(assigns)
 | 
					      |> assign(assigns)
 | 
				
			||||||
      |> assign(columns: columns, rows: rows, last_sort_key: initial_key, sort_mode: :asc)
 | 
					      |> assign(
 | 
				
			||||||
 | 
					        columns: columns,
 | 
				
			||||||
 | 
					        rows: rows,
 | 
				
			||||||
 | 
					        last_sort_key: initial_key,
 | 
				
			||||||
 | 
					        sort_mode: initial_sort_mode
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      |> assign_new(:row_class, fn -> "bg-white" end)
 | 
				
			||||||
 | 
					      |> assign_new(:alternate_row_class, fn -> "bg-gray-200" end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:ok, socket}
 | 
					    {:ok, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -56,20 +81,19 @@ defmodule CanneryWeb.Components.TableComponent do
 | 
				
			|||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "sort_by",
 | 
					        "sort_by",
 | 
				
			||||||
        %{"sort-key" => key},
 | 
					        %{"sort-key" => key},
 | 
				
			||||||
        %{assigns: %{rows: rows, last_sort_key: key, sort_mode: sort_mode}} = socket
 | 
					        %{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    sort_mode = if sort_mode == :asc, do: :desc, else: :asc
 | 
					    key = key |> String.to_existing_atom()
 | 
				
			||||||
    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(sort_mode: sort_mode, rows: rows)}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event(
 | 
					    sort_mode =
 | 
				
			||||||
        "sort_by",
 | 
					      case {key, sort_mode} do
 | 
				
			||||||
        %{"sort-key" => key},
 | 
					        {^last_sort_key, :asc} -> :desc
 | 
				
			||||||
        %{assigns: %{rows: rows}} = socket
 | 
					        {^last_sort_key, :desc} -> :asc
 | 
				
			||||||
      ) do
 | 
					        {_new_sort_key, _last_sort_mode} -> :asc
 | 
				
			||||||
    rows = rows |> sort_by_custom_sort_value_or_value(key, :asc)
 | 
					      end
 | 
				
			||||||
    {:noreply, socket |> assign(last_sort_key: key, sort_mode: :asc, rows: rows)}
 | 
					
 | 
				
			||||||
 | 
					    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
 | 
				
			||||||
 | 
					    {:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
 | 
				
			||||||
  end
 | 
					  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) do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
 | 
					<div id={@id} class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-white">
 | 
				
			||||||
  <table class="min-w-full table-auto text-center bg-white">
 | 
					  <table class="min-w-full table-auto text-center bg-white">
 | 
				
			||||||
    <thead class="border-b border-primary-600">
 | 
					    <thead class="border-b border-primary-600">
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
@@ -6,26 +6,27 @@
 | 
				
			|||||||
          <%= if column |> Map.get(:sortable, true) do %>
 | 
					          <%= if column |> Map.get(:sortable, true) do %>
 | 
				
			||||||
            <th class={"p-2 #{column[:class]}"}>
 | 
					            <th class={"p-2 #{column[:class]}"}>
 | 
				
			||||||
              <span
 | 
					              <span
 | 
				
			||||||
                class="cursor-pointer"
 | 
					                class="cursor-pointer flex justify-center items-center space-x-2"
 | 
				
			||||||
                phx-click="sort_by"
 | 
					                phx-click="sort_by"
 | 
				
			||||||
                phx-value-sort-key={key}
 | 
					                phx-value-sort-key={key}
 | 
				
			||||||
                phx-target={@myself}
 | 
					                phx-target={@myself}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <span class="underline"><%= label %></span>
 | 
					                <i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
 | 
				
			||||||
 | 
					                <span class={if @last_sort_key == key, do: "underline"}><%= label %></span>
 | 
				
			||||||
                <%= if @last_sort_key == key do %>
 | 
					                <%= if @last_sort_key == key do %>
 | 
				
			||||||
                  <%= case @sort_mode do %>
 | 
					                  <%= case @sort_mode do %>
 | 
				
			||||||
                    <% :asc -> %>
 | 
					                    <% :asc -> %>
 | 
				
			||||||
                      <i class="fas fa-sm fa-chevron-down"></i>
 | 
					                      <i class="w-0 float-right fas fa-sm fa-chevron-down"></i>
 | 
				
			||||||
                    <% :desc -> %>
 | 
					                    <% :desc -> %>
 | 
				
			||||||
                      <i class="fas fa-sm fa-chevron-up"></i>
 | 
					                      <i class="w-0 float-right fas fa-sm fa-chevron-up"></i>
 | 
				
			||||||
                  <% end %>
 | 
					                  <% end %>
 | 
				
			||||||
                <% else %>
 | 
					                <% else %>
 | 
				
			||||||
                  <i class="fas fa-sm fa-chevron-up opacity-0"></i>
 | 
					                  <i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
 | 
				
			||||||
                <% end %>
 | 
					                <% end %>
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </th>
 | 
					            </th>
 | 
				
			||||||
          <% else %>
 | 
					          <% else %>
 | 
				
			||||||
            <th class={"p-2 #{column[:class]}"}>
 | 
					            <th class={"p-2 cursor-not-allowed #{column[:class]}"}>
 | 
				
			||||||
              <%= label %>
 | 
					              <%= label %>
 | 
				
			||||||
            </th>
 | 
					            </th>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
@@ -33,8 +34,8 @@
 | 
				
			|||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
    </thead>
 | 
					    </thead>
 | 
				
			||||||
    <tbody>
 | 
					    <tbody>
 | 
				
			||||||
      <%= for values <- @rows do %>
 | 
					      <%= for {values, i} <- @rows |> Enum.with_index() do %>
 | 
				
			||||||
        <tr>
 | 
					        <tr class={if i |> Integer.is_even(), do: @row_class, else: @alternate_row_class}>
 | 
				
			||||||
          <%= for %{key: key} = value <- @columns do %>
 | 
					          <%= for %{key: key} = value <- @columns do %>
 | 
				
			||||||
            <td class={"p-2 #{value[:class]}"}>
 | 
					            <td class={"p-2 #{value[:class]}"}>
 | 
				
			||||||
              <%= case values |> Map.get(key) do %>
 | 
					              <%= case values |> Map.get(key) do %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,12 +4,16 @@ defmodule CanneryWeb.Components.TagCard do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
 | 
					  alias Cannery.Tags.Tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :tag, Tag, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block, required: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def tag_card(assigns) do
 | 
					  def tag_card(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      id={"tag-#{@tag.id}"}
 | 
					      id={"tag-#{@tag.id}"}
 | 
				
			||||||
      class="mx-4 my-2 px-8 py-4 space-x-4 flex justify-center items-center
 | 
					      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
 | 
					          border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
          transition-all duration-300 ease-in-out"
 | 
					          transition-all duration-300 ease-in-out"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
@@ -19,6 +23,8 @@ defmodule CanneryWeb.Components.TagCard do
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :tag, Tag, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def simple_tag_card(assigns) do
 | 
					  def simple_tag_card(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <h1
 | 
					    <h1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,10 +16,17 @@ defmodule CanneryWeb.Components.Topbar do
 | 
				
			|||||||
    <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-400">
 | 
					    <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="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">
 | 
					        <div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
 | 
				
			||||||
          <%= live_redirect("Cannery",
 | 
					          <.link
 | 
				
			||||||
            to: Routes.live_path(Endpoint, HomeLive),
 | 
					            navigate={Routes.live_path(Endpoint, HomeLive)}
 | 
				
			||||||
            class: "mx-2 my-1 leading-5 text-xl text-white hover:underline"
 | 
					            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 %>
 | 
					          <%= if @title_content do %>
 | 
				
			||||||
            <span class="mx-2 my-1">
 | 
					            <span class="mx-2 my-1">
 | 
				
			||||||
@@ -35,78 +42,100 @@ defmodule CanneryWeb.Components.Topbar do
 | 
				
			|||||||
          text-lg text-white text-ellipsis">
 | 
					          text-lg text-white text-ellipsis">
 | 
				
			||||||
          <%= if @current_user do %>
 | 
					          <%= if @current_user do %>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(gettext("Tags"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.tag_index_path(Endpoint, :index),
 | 
					                navigate={Routes.tag_index_path(Endpoint, :index)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline"
 | 
					                class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= gettext("Tags") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(gettext("Containers"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.container_index_path(Endpoint, :index),
 | 
					                navigate={Routes.container_index_path(Endpoint, :index)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline"
 | 
					                class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= gettext("Containers") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(gettext("Ammo"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.ammo_type_index_path(Endpoint, :index),
 | 
					                navigate={Routes.ammo_type_index_path(Endpoint, :index)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline"
 | 
					                class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= gettext("Catalog") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(gettext("Manage"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.ammo_group_index_path(Endpoint, :index),
 | 
					                navigate={Routes.ammo_group_index_path(Endpoint, :index)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline"
 | 
					                class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= gettext("Ammo") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(gettext("Range"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.range_index_path(Endpoint, :index),
 | 
					                navigate={Routes.range_index_path(Endpoint, :index)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline"
 | 
					                class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= gettext("Range") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <%= if @current_user.role == :admin do %>
 | 
					            <%= if @current_user.role == :admin do %>
 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					              <li class="mx-2 my-1">
 | 
				
			||||||
                <%= live_redirect(gettext("Invites"),
 | 
					                <.link
 | 
				
			||||||
                  to: Routes.invite_index_path(Endpoint, :index),
 | 
					                  navigate={Routes.invite_index_path(Endpoint, :index)}
 | 
				
			||||||
                  class: "text-primary-600 text-white hover:underline"
 | 
					                  class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
                ) %>
 | 
					                >
 | 
				
			||||||
 | 
					                  <%= gettext("Invites") %>
 | 
				
			||||||
 | 
					                </.link>
 | 
				
			||||||
              </li>
 | 
					              </li>
 | 
				
			||||||
            <% end %>
 | 
					            <% end %>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(@current_user.email,
 | 
					              <.link
 | 
				
			||||||
                to: Routes.user_settings_path(Endpoint, :edit),
 | 
					                navigate={Routes.user_settings_path(Endpoint, :edit)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline truncate"
 | 
					                class="text-primary-600 text-white hover:underline truncate"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= @current_user.email %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= link to: Routes.user_session_path(Endpoint, :delete),
 | 
					              <.link
 | 
				
			||||||
                   method: :delete,
 | 
					                href={Routes.user_session_path(Endpoint, :delete)}
 | 
				
			||||||
                   data: [confirm: dgettext("prompts", "Are you sure you want to log out?")] do %>
 | 
					                method="delete"
 | 
				
			||||||
 | 
					                data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
                <i class="fas fa-sign-out-alt"></i>
 | 
					                <i class="fas fa-sign-out-alt"></i>
 | 
				
			||||||
              <% end %>
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <%= if @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) do %>
 | 
					            <%= if @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) do %>
 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					              <li class="mx-2 my-1">
 | 
				
			||||||
                <%= live_redirect to: Routes.live_dashboard_path(Endpoint, :home),
 | 
					                <.link
 | 
				
			||||||
                  class: "text-primary-600 text-white hover:underline" do %>
 | 
					                  navigate={Routes.live_dashboard_path(Endpoint, :home)}
 | 
				
			||||||
                  <i class="fas fa-tachometer-alt"></i>
 | 
					                  class="text-primary-600 text-white hover:underline"
 | 
				
			||||||
                <% end %>
 | 
					                >
 | 
				
			||||||
 | 
					                  <i class="fas fa-gauge"></i>
 | 
				
			||||||
 | 
					                </.link>
 | 
				
			||||||
              </li>
 | 
					              </li>
 | 
				
			||||||
            <% end %>
 | 
					            <% end %>
 | 
				
			||||||
          <% else %>
 | 
					          <% else %>
 | 
				
			||||||
            <%= if Accounts.allow_registration?() do %>
 | 
					            <%= if Accounts.allow_registration?() do %>
 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					              <li class="mx-2 my-1">
 | 
				
			||||||
                <%= live_redirect(dgettext("actions", "Register"),
 | 
					                <.link
 | 
				
			||||||
                  to: Routes.user_registration_path(Endpoint, :new),
 | 
					                  navigate={Routes.user_registration_path(Endpoint, :new)}
 | 
				
			||||||
                  class: "text-primary-600 text-white hover:underline truncate"
 | 
					                  class="text-primary-600 text-white hover:underline truncate"
 | 
				
			||||||
                ) %>
 | 
					                >
 | 
				
			||||||
 | 
					                  <%= dgettext("actions", "Register") %>
 | 
				
			||||||
 | 
					                </.link>
 | 
				
			||||||
              </li>
 | 
					              </li>
 | 
				
			||||||
            <% end %>
 | 
					            <% end %>
 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					            <li class="mx-2 my-1">
 | 
				
			||||||
              <%= live_redirect(dgettext("actions", "Log in"),
 | 
					              <.link
 | 
				
			||||||
                to: Routes.user_session_path(Endpoint, :new),
 | 
					                navigate={Routes.user_session_path(Endpoint, :new)}
 | 
				
			||||||
                class: "text-primary-600 text-white hover:underline truncate"
 | 
					                class="text-primary-600 text-white hover:underline truncate"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= dgettext("actions", "Log in") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,10 @@ defmodule CanneryWeb.Components.UserCard do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
 | 
					  alias Cannery.Accounts.User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :user, User, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block, required: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def user_card(assigns) do
 | 
					  def user_card(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
@@ -18,12 +22,18 @@ defmodule CanneryWeb.Components.UserCard do
 | 
				
			|||||||
      </h1>
 | 
					      </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
					      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
				
			||||||
        <%= if @user.confirmed_at |> is_nil() do %>
 | 
					        <p>
 | 
				
			||||||
          Email unconfirmed
 | 
					          <%= if @user.confirmed_at |> is_nil() do %>
 | 
				
			||||||
        <% else %>
 | 
					            Email unconfirmed
 | 
				
			||||||
          <p>User was confirmed at</p>
 | 
					          <% else %>
 | 
				
			||||||
          <%= @user.confirmed_at |> display_datetime() %>
 | 
					            User was confirmed at <%= @user.confirmed_at |> display_datetime() %>
 | 
				
			||||||
        <% end %>
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>
 | 
				
			||||||
 | 
					          <%= gettext("User registered on") %>
 | 
				
			||||||
 | 
					          <%= @user.inserted_at |> display_datetime() %>
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
      </h3>
 | 
					      </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= if @inner_block do %>
 | 
					      <%= if @inner_block do %>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										69
									
								
								lib/cannery_web/controllers/export_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/cannery_web/controllers/export_controller.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.ExportController do
 | 
				
			||||||
 | 
					  use CanneryWeb, :controller
 | 
				
			||||||
 | 
					  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_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
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      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_group
 | 
				
			||||||
 | 
					        |> Jason.encode!()
 | 
				
			||||||
 | 
					        |> Jason.decode!()
 | 
				
			||||||
 | 
					        |> Map.merge(%{
 | 
				
			||||||
 | 
					          "used_count" => used_count,
 | 
				
			||||||
 | 
					          "percentage_remaining" => percentage_remaining,
 | 
				
			||||||
 | 
					          "original_count" => original_count,
 | 
				
			||||||
 | 
					          "cpr" => cpr
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    shot_groups = ActivityLog.list_shot_groups(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!()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        container
 | 
				
			||||||
 | 
					        |> Jason.encode!()
 | 
				
			||||||
 | 
					        |> Jason.decode!()
 | 
				
			||||||
 | 
					        |> Map.merge(%{
 | 
				
			||||||
 | 
					          "ammo_group_count" => ammo_group_count,
 | 
				
			||||||
 | 
					          "round_count" => round_count
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    json(conn, %{
 | 
				
			||||||
 | 
					      user: current_user,
 | 
				
			||||||
 | 
					      ammo_types: ammo_types,
 | 
				
			||||||
 | 
					      ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					      shot_groups: shot_groups,
 | 
				
			||||||
 | 
					      containers: containers
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -2,7 +2,6 @@ defmodule CanneryWeb.UserRegistrationController do
 | 
				
			|||||||
  use CanneryWeb, :controller
 | 
					  use CanneryWeb, :controller
 | 
				
			||||||
  import CanneryWeb.Gettext
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  alias Cannery.{Accounts, Invites}
 | 
					  alias Cannery.{Accounts, Invites}
 | 
				
			||||||
  alias Cannery.Accounts.User
 | 
					 | 
				
			||||||
  alias CanneryWeb.{Endpoint, HomeLive}
 | 
					  alias CanneryWeb.{Endpoint, HomeLive}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, %{"invite" => invite_token}) do
 | 
					  def new(conn, %{"invite" => invite_token}) do
 | 
				
			||||||
@@ -30,7 +29,7 @@ defmodule CanneryWeb.UserRegistrationController do
 | 
				
			|||||||
  # renders new user registration page
 | 
					  # renders new user registration page
 | 
				
			||||||
  defp render_new(conn, invite \\ nil) do
 | 
					  defp render_new(conn, invite \\ nil) do
 | 
				
			||||||
    render(conn, "new.html",
 | 
					    render(conn, "new.html",
 | 
				
			||||||
      changeset: Accounts.change_user_registration(%User{}),
 | 
					      changeset: Accounts.change_user_registration(),
 | 
				
			||||||
      invite: invite,
 | 
					      invite: invite,
 | 
				
			||||||
      page_title: gettext("Register")
 | 
					      page_title: gettext("Register")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,10 +10,11 @@ defmodule CanneryWeb.UserSettingsController do
 | 
				
			|||||||
    render(conn, "edit.html", page_title: gettext("Settings"))
 | 
					    render(conn, "edit.html", page_title: gettext("Settings"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update(conn, %{"action" => "update_email"} = params) do
 | 
					  def update(%{assigns: %{current_user: user}} = conn, %{
 | 
				
			||||||
    %{"current_password" => password, "user" => user_params} = params
 | 
					        "action" => "update_email",
 | 
				
			||||||
    user = conn.assigns.current_user
 | 
					        "current_password" => password,
 | 
				
			||||||
 | 
					        "user" => user_params
 | 
				
			||||||
 | 
					      }) do
 | 
				
			||||||
    case Accounts.apply_user_email(user, password, user_params) do
 | 
					    case Accounts.apply_user_email(user, password, user_params) do
 | 
				
			||||||
      {:ok, applied_user} ->
 | 
					      {:ok, applied_user} ->
 | 
				
			||||||
        Accounts.deliver_update_email_instructions(
 | 
					        Accounts.deliver_update_email_instructions(
 | 
				
			||||||
@@ -33,14 +34,15 @@ defmodule CanneryWeb.UserSettingsController do
 | 
				
			|||||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        render(conn, "edit.html", email_changeset: changeset)
 | 
					        conn |> render("edit.html", email_changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update(conn, %{"action" => "update_password"} = params) do
 | 
					  def update(%{assigns: %{current_user: user}} = conn, %{
 | 
				
			||||||
    %{"current_password" => password, "user" => user_params} = params
 | 
					        "action" => "update_password",
 | 
				
			||||||
    user = conn.assigns.current_user
 | 
					        "current_password" => password,
 | 
				
			||||||
 | 
					        "user" => user_params
 | 
				
			||||||
 | 
					      }) do
 | 
				
			||||||
    case Accounts.update_user_password(user, password, user_params) do
 | 
					    case Accounts.update_user_password(user, password, user_params) do
 | 
				
			||||||
      {:ok, user} ->
 | 
					      {:ok, user} ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
@@ -49,12 +51,27 @@ defmodule CanneryWeb.UserSettingsController do
 | 
				
			|||||||
        |> UserAuth.log_in_user(user)
 | 
					        |> UserAuth.log_in_user(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        render(conn, "edit.html", password_changeset: changeset)
 | 
					        conn |> render("edit.html", password_changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def confirm_email(conn, %{"token" => token}) do
 | 
					  def update(
 | 
				
			||||||
    case Accounts.update_user_email(conn.assigns.current_user, token) do
 | 
					        %{assigns: %{current_user: user}} = conn,
 | 
				
			||||||
 | 
					        %{"action" => "update_locale", "user" => %{"locale" => locale}}
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    case Accounts.update_user_locale(user, locale) do
 | 
				
			||||||
 | 
					      {:ok, _user} ->
 | 
				
			||||||
 | 
					        conn
 | 
				
			||||||
 | 
					        |> put_flash(:info, dgettext("prompts", "Language updated successfully."))
 | 
				
			||||||
 | 
					        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:error, changeset} ->
 | 
				
			||||||
 | 
					        conn |> render("edit.html", locale_changeset: changeset)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def confirm_email(%{assigns: %{current_user: user}} = conn, %{"token" => token}) do
 | 
				
			||||||
 | 
					    case Accounts.update_user_email(user, token) do
 | 
				
			||||||
      :ok ->
 | 
					      :ok ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Email changed successfully."))
 | 
					        |> put_flash(:info, dgettext("prompts", "Email changed successfully."))
 | 
				
			||||||
@@ -84,11 +101,10 @@ defmodule CanneryWeb.UserSettingsController do
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp assign_email_and_password_changesets(conn, _opts) do
 | 
					  defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do
 | 
				
			||||||
    user = conn.assigns.current_user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    conn
 | 
					    conn
 | 
				
			||||||
    |> assign(:email_changeset, Accounts.change_user_email(user))
 | 
					    |> assign(:email_changeset, Accounts.change_user_email(user))
 | 
				
			||||||
    |> assign(:password_changeset, Accounts.change_user_password(user))
 | 
					    |> assign(:password_changeset, Accounts.change_user_password(user))
 | 
				
			||||||
 | 
					    |> assign(:locale_changeset, Accounts.change_user_locale(user))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,38 +21,30 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec update(Socket.t()) :: {:ok, Socket.t()}
 | 
					  @spec update(Socket.t()) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket) do
 | 
					  def update(%{assigns: %{current_user: current_user}} = socket) do
 | 
				
			||||||
    socket =
 | 
					    %{assigns: %{ammo_types: ammo_types, containers: containers}} =
 | 
				
			||||||
 | 
					      socket =
 | 
				
			||||||
      socket
 | 
					      socket
 | 
				
			||||||
      |> assign(:ammo_group_create_limit, @ammo_group_create_limit)
 | 
					      |> assign(:ammo_group_create_limit, @ammo_group_create_limit)
 | 
				
			||||||
      |> assign(:changeset, Ammo.change_ammo_group(ammo_group))
 | 
					 | 
				
			||||||
      |> assign(:ammo_types, Ammo.list_ammo_types(current_user))
 | 
					      |> assign(:ammo_types, Ammo.list_ammo_types(current_user))
 | 
				
			||||||
      |> assign_new(:containers, fn -> Containers.list_containers(current_user) end)
 | 
					      |> assign_new(:containers, fn -> Containers.list_containers(current_user) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:ok, socket}
 | 
					    params =
 | 
				
			||||||
 | 
					      if ammo_types |> List.first() |> is_nil(),
 | 
				
			||||||
 | 
					        do: %{},
 | 
				
			||||||
 | 
					        else: %{} |> Map.put("ammo_type_id", ammo_types |> List.first() |> Map.get(:id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    params =
 | 
				
			||||||
 | 
					      if containers |> List.first() |> is_nil(),
 | 
				
			||||||
 | 
					        do: params,
 | 
				
			||||||
 | 
					        else: params |> Map.put("container_id", containers |> List.first() |> Map.get(:id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket |> assign_changeset(params)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event("validate", %{"ammo_group" => ammo_group_params}, socket) do
 | 
				
			||||||
        "validate",
 | 
					    {:noreply, socket |> assign_changeset(ammo_group_params)}
 | 
				
			||||||
        %{"ammo_group" => ammo_group_params},
 | 
					 | 
				
			||||||
        %{assigns: %{action: action, ammo_group: ammo_group}} = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    changeset_action =
 | 
					 | 
				
			||||||
      case action do
 | 
					 | 
				
			||||||
        :new -> :insert
 | 
					 | 
				
			||||||
        :edit -> :update
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    changeset = ammo_group |> Ammo.change_ammo_group(ammo_group_params)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    changeset =
 | 
					 | 
				
			||||||
      case changeset |> Changeset.apply_action(changeset_action) do
 | 
					 | 
				
			||||||
        {:ok, _data} -> changeset
 | 
					 | 
				
			||||||
        {:error, changeset} -> changeset
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, changeset)}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
@@ -76,6 +68,44 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  # Save Helpers
 | 
					  # Save Helpers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp assign_changeset(
 | 
				
			||||||
 | 
					         %{assigns: %{action: action, ammo_group: ammo_group, current_user: user}} = socket,
 | 
				
			||||||
 | 
					         ammo_group_params
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    changeset_action =
 | 
				
			||||||
 | 
					      cond do
 | 
				
			||||||
 | 
					        action in [:new, :clone] -> :insert
 | 
				
			||||||
 | 
					        action == :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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ammo_group |> AmmoGroup.create_changeset(ammo_type, container, user, ammo_group_params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        action == :edit ->
 | 
				
			||||||
 | 
					          ammo_group |> AmmoGroup.update_changeset(ammo_group_params, user)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case changeset |> Changeset.apply_action(changeset_action) do
 | 
				
			||||||
 | 
					        {:ok, _data} -> changeset
 | 
				
			||||||
 | 
					        {:error, changeset} -> changeset
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(:changeset, changeset)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp save_ammo_group(
 | 
					  defp save_ammo_group(
 | 
				
			||||||
         %{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} =
 | 
					         %{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} =
 | 
				
			||||||
           socket,
 | 
					           socket,
 | 
				
			||||||
@@ -85,8 +115,8 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      case Ammo.update_ammo_group(ammo_group, ammo_group_params, current_user) do
 | 
					      case Ammo.update_ammo_group(ammo_group, ammo_group_params, current_user) do
 | 
				
			||||||
        {:ok, _ammo_group} ->
 | 
					        {:ok, _ammo_group} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "Ammo group updated successfully")
 | 
					          prompt = dgettext("prompts", "Ammo updated successfully")
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
@@ -97,9 +127,10 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  defp save_ammo_group(
 | 
					  defp save_ammo_group(
 | 
				
			||||||
         %{assigns: %{changeset: changeset}} = socket,
 | 
					         %{assigns: %{changeset: changeset}} = socket,
 | 
				
			||||||
         :new,
 | 
					         action,
 | 
				
			||||||
         %{"multiplier" => multiplier_str} = ammo_group_params
 | 
					         %{"multiplier" => multiplier_str} = ammo_group_params
 | 
				
			||||||
       ) do
 | 
					       )
 | 
				
			||||||
 | 
					       when action in [:new, :clone] do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      case multiplier_str |> Integer.parse() do
 | 
					      case multiplier_str |> Integer.parse() do
 | 
				
			||||||
        {multiplier, _remainder}
 | 
					        {multiplier, _remainder}
 | 
				
			||||||
@@ -146,12 +177,12 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
        prompt =
 | 
					        prompt =
 | 
				
			||||||
          dngettext(
 | 
					          dngettext(
 | 
				
			||||||
            "prompts",
 | 
					            "prompts",
 | 
				
			||||||
            "Ammo group created successfully",
 | 
					            "Ammo added successfully",
 | 
				
			||||||
            "Ammo groups created successfully",
 | 
					            "Ammo added successfully",
 | 
				
			||||||
            count
 | 
					            count
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					        socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, %Changeset{} = changeset} ->
 | 
					      {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
        socket |> assign(changeset: changeset)
 | 
					        socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="ammo_group-form"
 | 
					    id="ammo_group-form"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
@@ -38,6 +38,14 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :price_paid, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :price_paid, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= label(f, :purchased_on, gettext("Purchased on"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
 | 
					    <%= date_input(f, :purchased_on,
 | 
				
			||||||
 | 
					      class: "input input-primary col-span-2",
 | 
				
			||||||
 | 
					      phx_update: "ignore",
 | 
				
			||||||
 | 
					      value: @changeset |> Changeset.get_field(:purchased_on) || Date.utc_today()
 | 
				
			||||||
 | 
					    ) %>
 | 
				
			||||||
 | 
					    <%= error_tag(f, :purchased_on, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
 | 
					    <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= textarea(f, :notes,
 | 
					    <%= textarea(f, :notes,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
@@ -51,8 +59,8 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :container_id, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :container_id, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= case @action do %>
 | 
					    <%= cond do %>
 | 
				
			||||||
      <% :new -> %>
 | 
					      <% @action in [:new, :clone] -> %>
 | 
				
			||||||
        <hr class="hr col-span-3" />
 | 
					        <hr class="hr col-span-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %>
 | 
					        <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
@@ -69,7 +77,7 @@
 | 
				
			|||||||
        ) %>
 | 
					        ) %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= error_tag(f, :multiplier, "col-span-3 text-center") %>
 | 
					        <%= error_tag(f, :multiplier, "col-span-3 text-center") %>
 | 
				
			||||||
      <% :edit -> %>
 | 
					      <% @action == :edit -> %>
 | 
				
			||||||
        <%= submit(dgettext("actions", "Save"),
 | 
					        <%= submit(dgettext("actions", "Save"),
 | 
				
			||||||
          phx_disable_with: dgettext("prompts", "Saving..."),
 | 
					          phx_disable_with: dgettext("prompts", "Saving..."),
 | 
				
			||||||
          class: "mx-auto col-span-3 btn btn-primary"
 | 
					          class: "mx-auto col-span-3 btn btn-primary"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,17 +4,20 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  alias Cannery.{Ammo, Ammo.AmmoGroup, Containers, Repo}
 | 
					  alias Cannery.{Ammo, Ammo.AmmoGroup, Containers}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(%{"search" => search}, _session, socket) do
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session) |> display_ammo_groups()}
 | 
					    {:ok, socket |> assign(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()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
					  def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
				
			||||||
    {:noreply, apply_action(socket, live_action, params)}
 | 
					    {:noreply, apply_action(socket, live_action, params) |> display_ammo_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(
 | 
					  defp apply_action(
 | 
				
			||||||
@@ -23,37 +26,67 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
 | 
				
			|||||||
         %{"id" => id}
 | 
					         %{"id" => id}
 | 
				
			||||||
       ) do
 | 
					       ) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Record shots"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_group, Ammo.get_ammo_group!(id, current_user))
 | 
					      page_title: gettext("Record shots"),
 | 
				
			||||||
 | 
					      ammo_group: Ammo.get_ammo_group!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :move, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :move, %{"id" => id}) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Move Ammo group"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_group, Ammo.get_ammo_group!(id, current_user))
 | 
					      page_title: gettext("Move ammo"),
 | 
				
			||||||
 | 
					      ammo_group: Ammo.get_ammo_group!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Edit Ammo group"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_group, Ammo.get_ammo_group!(id, current_user))
 | 
					      page_title: gettext("Edit ammo"),
 | 
				
			||||||
 | 
					      ammo_group: Ammo.get_ammo_group!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :clone, %{"id" => id}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: dgettext("actions", "Add Ammo"),
 | 
				
			||||||
 | 
					      ammo_group: %{Ammo.get_ammo_group!(id, current_user) | id: nil}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :new, _params) do
 | 
					  defp apply_action(socket, :new, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, dgettext("actions", "Add Ammo"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_group, %AmmoGroup{})
 | 
					      page_title: dgettext("actions", "Add Ammo"),
 | 
				
			||||||
 | 
					      ammo_group: %AmmoGroup{}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket |> assign(:page_title, gettext("Ammo groups")) |> assign(:ammo_group, nil)
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Ammo"),
 | 
				
			||||||
 | 
					      search: nil,
 | 
				
			||||||
 | 
					      ammo_group: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(socket, :search, %{"search" => search}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Ammo"),
 | 
				
			||||||
 | 
					      search: search,
 | 
				
			||||||
 | 
					      ammo_group: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
 | 
					  def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
 | 
				
			||||||
    Ammo.get_ammo_group!(id, current_user) |> Ammo.delete_ammo_group!(current_user)
 | 
					    Ammo.get_ammo_group!(id, current_user) |> Ammo.delete_ammo_group!(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prompt = dgettext("prompts", "Ammo group deleted succesfully")
 | 
					    prompt = dgettext("prompts", "Ammo deleted succesfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_groups()}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -72,123 +105,35 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
 | 
				
			|||||||
    {:noreply, socket |> display_ammo_groups()}
 | 
					    {:noreply, socket |> display_ammo_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp display_ammo_groups(%{assigns: %{current_user: current_user}} = socket) do
 | 
					  @impl true
 | 
				
			||||||
    ammo_groups = Ammo.list_ammo_groups(current_user) |> Repo.preload([:ammo_type, :container])
 | 
					  def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
 | 
				
			||||||
    containers = Containers.list_containers(current_user)
 | 
					    {:noreply, socket |> assign(:show_used, !show_used) |> display_ammo_groups()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns = [
 | 
					  @impl true
 | 
				
			||||||
      %{label: gettext("Ammo type"), key: "ammo_type"},
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
      %{label: gettext("Count"), key: "count"},
 | 
					    {:noreply, socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :index))}
 | 
				
			||||||
      %{label: gettext("Price paid"), key: "price_paid"},
 | 
					  end
 | 
				
			||||||
      %{label: gettext("% left"), key: "remaining"},
 | 
					 | 
				
			||||||
      %{label: gettext("Range"), key: "range"},
 | 
					 | 
				
			||||||
      %{label: gettext("Container"), key: "container"},
 | 
					 | 
				
			||||||
      %{label: nil, key: "actions", sortable: false}
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rows =
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
      ammo_groups
 | 
					    socket =
 | 
				
			||||||
      |> Enum.map(fn ammo_group -> ammo_group |> get_row_data_for_ammo_group(columns) end)
 | 
					      socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :search, search_term))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_groups(
 | 
				
			||||||
 | 
					         %{assigns: %{search: search, current_user: current_user, show_used: show_used}} = socket
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    ammo_groups = Ammo.list_ammo_groups(search, show_used, current_user)
 | 
				
			||||||
 | 
					    ammo_types_count = Ammo.get_ammo_types_count!(current_user)
 | 
				
			||||||
 | 
					    containers_count = Containers.get_containers_count!(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(ammo_groups: ammo_groups, containers: containers, columns: columns, rows: rows)
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					      ammo_types_count: ammo_types_count,
 | 
				
			||||||
 | 
					      containers_count: containers_count
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @spec get_row_data_for_ammo_group(AmmoGroup.t(), [map()]) :: [map()]
 | 
					 | 
				
			||||||
  defp get_row_data_for_ammo_group(ammo_group, columns) do
 | 
					 | 
				
			||||||
    ammo_group = ammo_group |> Repo.preload([:ammo_type, :container])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    columns
 | 
					 | 
				
			||||||
    |> Enum.into(%{}, fn %{key: key} -> {key, get_value_for_key(key, ammo_group)} end)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @spec get_value_for_key(String.t(), AmmoGroup.t()) :: any()
 | 
					 | 
				
			||||||
  defp get_value_for_key("ammo_type", %{ammo_type: ammo_type}) do
 | 
					 | 
				
			||||||
    {ammo_type.name,
 | 
					 | 
				
			||||||
     live_patch(ammo_type.name,
 | 
					 | 
				
			||||||
       to: Routes.ammo_type_show_path(Endpoint, :show, ammo_type),
 | 
					 | 
				
			||||||
       class: "link"
 | 
					 | 
				
			||||||
     )}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("price_paid", %{price_paid: nil}), do: {"a", nil}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("price_paid", %{price_paid: price_paid}),
 | 
					 | 
				
			||||||
    do: gettext("$%{amount}", amount: price_paid |> :erlang.float_to_binary(decimals: 2))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("range", %{staged: staged} = ammo_group) do
 | 
					 | 
				
			||||||
    assigns = %{ammo_group: ammo_group}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {staged,
 | 
					 | 
				
			||||||
     ~H"""
 | 
					 | 
				
			||||||
     <div class="min-w-20 py-2 px-4 h-full flex flex-col justify-center items-center">
 | 
					 | 
				
			||||||
       <button
 | 
					 | 
				
			||||||
         type="button"
 | 
					 | 
				
			||||||
         class="mx-2 my-1 btn btn-primary"
 | 
					 | 
				
			||||||
         phx-click="toggle_staged"
 | 
					 | 
				
			||||||
         phx-value-ammo_group_id={ammo_group.id}
 | 
					 | 
				
			||||||
       >
 | 
					 | 
				
			||||||
         <%= if ammo_group.staged, do: gettext("Unstage"), else: gettext("Stage") %>
 | 
					 | 
				
			||||||
       </button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
       <%= live_patch(dgettext("actions", "Record shots"),
 | 
					 | 
				
			||||||
         to: Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group),
 | 
					 | 
				
			||||||
         class: "mx-2 my-1 btn btn-primary"
 | 
					 | 
				
			||||||
       ) %>
 | 
					 | 
				
			||||||
     </div>
 | 
					 | 
				
			||||||
     """}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("remaining", ammo_group),
 | 
					 | 
				
			||||||
    do: "#{ammo_group |> Ammo.get_percentage_remaining()}%"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("actions", ammo_group) do
 | 
					 | 
				
			||||||
    assigns = %{ammo_group: ammo_group}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
      <%= live_redirect to: Routes.ammo_group_show_path(Endpoint, :show, ammo_group),
 | 
					 | 
				
			||||||
                    class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                    data: [qa: "view-#{ammo_group.id}"] do %>
 | 
					 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= live_patch to: Routes.ammo_group_index_path(Endpoint, :edit, ammo_group),
 | 
					 | 
				
			||||||
                  class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                  data: [qa: "edit-#{ammo_group.id}"] do %>
 | 
					 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= link to: "#",
 | 
					 | 
				
			||||||
            class: "text-primary-600 link",
 | 
					 | 
				
			||||||
            phx_click: "delete",
 | 
					 | 
				
			||||||
            phx_value_id: ammo_group.id,
 | 
					 | 
				
			||||||
            data: [
 | 
					 | 
				
			||||||
              confirm: dgettext("prompts", "Are you sure you want to delete this ammo?"),
 | 
					 | 
				
			||||||
              qa: "delete-#{ammo_group.id}"
 | 
					 | 
				
			||||||
            ] do %>
 | 
					 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("container", %{container: nil}), do: {nil, nil}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key("container", %{container: %{name: container_name}} = ammo_group) do
 | 
					 | 
				
			||||||
    assigns = %{ammo_group: ammo_group}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {container_name,
 | 
					 | 
				
			||||||
     ~H"""
 | 
					 | 
				
			||||||
     <div class="min-w-20 py-2 px-4 h-full space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
       <%= live_patch(@ammo_group.container.name,
 | 
					 | 
				
			||||||
         to: Routes.ammo_group_index_path(Endpoint, :move, @ammo_group),
 | 
					 | 
				
			||||||
         class: "btn btn-primary"
 | 
					 | 
				
			||||||
       ) %>
 | 
					 | 
				
			||||||
     </div>
 | 
					 | 
				
			||||||
     """}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_value_for_key(key, ammo_group),
 | 
					 | 
				
			||||||
    do: ammo_group |> Map.get(key |> String.to_existing_atom())
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,60 +3,165 @@
 | 
				
			|||||||
    <%= gettext("Ammo") %>
 | 
					    <%= gettext("Ammo") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= if @ammo_groups |> Enum.empty?() and @search |> is_nil() do %>
 | 
				
			||||||
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					      <%= gettext("No Ammo") %>
 | 
				
			||||||
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= cond do %>
 | 
				
			||||||
 | 
					    <% @containers_count == 0 -> %>
 | 
				
			||||||
 | 
					      <div class="flex justify-center items-center">
 | 
				
			||||||
 | 
					        <h2 class="m-2 title text-md text-primary-600">
 | 
				
			||||||
 | 
					          <%= dgettext("prompts", "You'll need to") %>
 | 
				
			||||||
 | 
					        </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					          <%= dgettext("actions", "add a container first") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    <% @ammo_types_count == 0 -> %>
 | 
				
			||||||
 | 
					      <div class="flex justify-center items-center">
 | 
				
			||||||
 | 
					        <h2 class="m-2 title text-md text-primary-600">
 | 
				
			||||||
 | 
					          <%= dgettext("prompts", "You'll need to") %>
 | 
				
			||||||
 | 
					        </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <.link navigate={Routes.ammo_type_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					          <%= dgettext("actions", "add an ammo type first") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    <% @ammo_groups |> Enum.empty?() and @search |> is_nil() -> %>
 | 
				
			||||||
 | 
					      <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Add your first box!") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					    <% true -> %>
 | 
				
			||||||
 | 
					      <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Add Ammo") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
				
			||||||
 | 
					    <.form
 | 
				
			||||||
 | 
					      :let={f}
 | 
				
			||||||
 | 
					      for={:search}
 | 
				
			||||||
 | 
					      phx-change="search"
 | 
				
			||||||
 | 
					      phx-submit="search"
 | 
				
			||||||
 | 
					      class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
 | 
					      data-qa="ammo_group_search"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					        class: "input input-primary",
 | 
				
			||||||
 | 
					        value: @search,
 | 
				
			||||||
 | 
					        phx_debounce: 300,
 | 
				
			||||||
 | 
					        placeholder: gettext("Search ammo")
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					    </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.toggle_button action="toggle_show_used" value={@show_used}>
 | 
				
			||||||
 | 
					      <span class="title text-lg text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("Show used") %>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    </.toggle_button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= if @ammo_groups |> Enum.empty?() do %>
 | 
					  <%= if @ammo_groups |> Enum.empty?() do %>
 | 
				
			||||||
    <h2 class="title text-xl text-primary-600">
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
      <%= gettext("No Ammo") %>
 | 
					      <%= gettext("No Ammo") %>
 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h2>
 | 
					    </h2>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= if @containers |> Enum.empty?() do %>
 | 
					 | 
				
			||||||
      <div class="flex justify-center items-center">
 | 
					 | 
				
			||||||
        <h2 class="m-2 title text-md text-primary-600">
 | 
					 | 
				
			||||||
          <%= dgettext("prompts", "You'll need to") %>
 | 
					 | 
				
			||||||
        </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <%= live_patch(dgettext("actions", "add a container first"),
 | 
					 | 
				
			||||||
          to: Routes.container_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
          class: "btn btn-primary"
 | 
					 | 
				
			||||||
        ) %>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    <% else %>
 | 
					 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Add your first box!"),
 | 
					 | 
				
			||||||
        to: Routes.ammo_group_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
        class: "btn btn-primary"
 | 
					 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= if @containers |> Enum.empty?() do %>
 | 
					 | 
				
			||||||
      <div class="flex justify-center items-center">
 | 
					 | 
				
			||||||
        <h2 class="m-2 title text-md text-primary-600">
 | 
					 | 
				
			||||||
          <%= dgettext("prompts", "You'll need to") %>
 | 
					 | 
				
			||||||
        </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <%= live_patch(dgettext("actions", "add a container first"),
 | 
					 | 
				
			||||||
          to: Routes.container_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
          class: "btn btn-primary"
 | 
					 | 
				
			||||||
        ) %>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    <% else %>
 | 
					 | 
				
			||||||
      <%= live_patch(dgettext("actions", "New Ammo group"),
 | 
					 | 
				
			||||||
        to: Routes.ammo_group_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
        class: "btn btn-primary"
 | 
					 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <.live_component
 | 
					    <.live_component
 | 
				
			||||||
      module={CanneryWeb.Components.TableComponent}
 | 
					      module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
      id="ammo_groups_index_table"
 | 
					      id="ammo-group-index-table"
 | 
				
			||||||
      action={@live_action}
 | 
					      ammo_groups={@ammo_groups}
 | 
				
			||||||
      columns={@columns}
 | 
					      current_user={@current_user}
 | 
				
			||||||
      rows={@rows}
 | 
					    >
 | 
				
			||||||
    />
 | 
					      <:ammo_type :let={%{name: ammo_type_name} = ammo_type}>
 | 
				
			||||||
 | 
					        <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link">
 | 
				
			||||||
 | 
					          <%= ammo_type_name %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </:ammo_type>
 | 
				
			||||||
 | 
					      <:range :let={ammo_group}>
 | 
				
			||||||
 | 
					        <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center">
 | 
				
			||||||
 | 
					          <button
 | 
				
			||||||
 | 
					            type="button"
 | 
				
			||||||
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
 | 
					            phx-click="toggle_staged"
 | 
				
			||||||
 | 
					            phx-value-ammo_group_id={ammo_group.id}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <%= if ammo_group.staged, do: gettext("Unstage"), else: gettext("Stage") %>
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group)}
 | 
				
			||||||
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <%= dgettext("actions", "Record shots") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </:range>
 | 
				
			||||||
 | 
					      <:container :let={%{container: %{name: container_name} = container} = ammo_group}>
 | 
				
			||||||
 | 
					        <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center">
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            navigate={Routes.container_show_path(Endpoint, :show, container)}
 | 
				
			||||||
 | 
					            class="mx-2 my-1 link"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <%= container_name %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)}
 | 
				
			||||||
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <%= gettext("Move ammo") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </:container>
 | 
				
			||||||
 | 
					      <:actions :let={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}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)}
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            data-qa={"edit-#{ammo_group.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)}
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            data-qa={"clone-#{ammo_group.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            href="#"
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            phx-click="delete"
 | 
				
			||||||
 | 
					            phx-value-id={ammo_group.id}
 | 
				
			||||||
 | 
					            data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
 | 
				
			||||||
 | 
					            data-qa={"delete-#{ammo_group.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </:actions>
 | 
				
			||||||
 | 
					    </.live_component>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= cond do %>
 | 
					<%= cond do %>
 | 
				
			||||||
  <% @live_action in [:new, :edit] -> %>
 | 
					  <% @live_action in [:new, :edit, :clone] -> %>
 | 
				
			||||||
    <.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
 | 
					    <.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}>
 | 
				
			||||||
      <.live_component
 | 
					      <.live_component
 | 
				
			||||||
        module={CanneryWeb.AmmoGroupLive.FormComponent}
 | 
					        module={CanneryWeb.AmmoGroupLive.FormComponent}
 | 
				
			||||||
@@ -66,7 +171,6 @@
 | 
				
			|||||||
        ammo_group={@ammo_group}
 | 
					        ammo_group={@ammo_group}
 | 
				
			||||||
        return_to={Routes.ammo_group_index_path(Endpoint, :index)}
 | 
					        return_to={Routes.ammo_group_index_path(Endpoint, :index)}
 | 
				
			||||||
        current_user={@current_user}
 | 
					        current_user={@current_user}
 | 
				
			||||||
        containers={@containers}
 | 
					 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </.modal>
 | 
					    </.modal>
 | 
				
			||||||
  <% @live_action == :add_shot_group -> %>
 | 
					  <% @live_action == :add_shot_group -> %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,7 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(_params, _session, socket), do: {:ok, socket}
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session)}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(
 | 
					  def handle_params(
 | 
				
			||||||
@@ -32,14 +30,19 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(%{"id" => id}, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
					  def handle_params(%{"id" => id}, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
				
			||||||
    {:noreply, socket |> assign(page_title: page_title(live_action)) |> display_ammo_group(id)}
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(page_title: page_title(live_action))
 | 
				
			||||||
 | 
					      |> display_ammo_group(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp page_title(:add_shot_group), do: gettext("Record Shots")
 | 
					  defp page_title(:add_shot_group), do: gettext("Record Shots")
 | 
				
			||||||
  defp page_title(:edit_shot_group), do: gettext("Edit Shot Records")
 | 
					  defp page_title(:edit_shot_group), do: gettext("Edit Shot Records")
 | 
				
			||||||
  defp page_title(:move), do: gettext("Move Ammo group")
 | 
					  defp page_title(:move), do: gettext("Move Ammo")
 | 
				
			||||||
  defp page_title(:show), do: gettext("Show Ammo group")
 | 
					  defp page_title(:show), do: gettext("Show Ammo")
 | 
				
			||||||
  defp page_title(:edit), do: gettext("Edit Ammo group")
 | 
					  defp page_title(:edit), do: gettext("Edit Ammo")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
@@ -49,10 +52,10 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    ammo_group |> Ammo.delete_ammo_group!(current_user)
 | 
					    ammo_group |> Ammo.delete_ammo_group!(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prompt = dgettext("prompts", "Ammo group deleted succesfully")
 | 
					    prompt = dgettext("prompts", "Ammo deleted succesfully")
 | 
				
			||||||
    redirect_to = Routes.ammo_group_index_path(socket, :index)
 | 
					    redirect_to = Routes.ammo_group_index_path(socket, :index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt) |> push_redirect(to: redirect_to)}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -71,14 +74,14 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "delete_shot_group",
 | 
					        "delete_shot_group",
 | 
				
			||||||
        %{"id" => id},
 | 
					        %{"id" => id},
 | 
				
			||||||
        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
					        %{assigns: %{ammo_group: %{id: ammo_group_id}, current_user: current_user}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    {:ok, _} =
 | 
					    {:ok, _} =
 | 
				
			||||||
      ActivityLog.get_shot_group!(id, current_user)
 | 
					      ActivityLog.get_shot_group!(id, current_user)
 | 
				
			||||||
      |> ActivityLog.delete_shot_group(current_user)
 | 
					      |> ActivityLog.delete_shot_group(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prompt = dgettext("prompts", "Shot records deleted succesfully")
 | 
					    prompt = dgettext("prompts", "Shot records deleted succesfully")
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_group(ammo_group)}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_group(ammo_group_id)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t()
 | 
					  @spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t()
 | 
				
			||||||
@@ -86,10 +89,10 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
    ammo_group = ammo_group |> Repo.preload([:container, :ammo_type, :shot_groups], force: true)
 | 
					    ammo_group = ammo_group |> Repo.preload([:container, :ammo_type, :shot_groups], force: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns = [
 | 
					    columns = [
 | 
				
			||||||
      %{label: gettext("Rounds shot"), key: "count"},
 | 
					      %{label: gettext("Rounds shot"), key: :count},
 | 
				
			||||||
      %{label: gettext("Notes"), key: "notes"},
 | 
					      %{label: gettext("Notes"), key: :notes},
 | 
				
			||||||
      %{label: gettext("Date"), key: "date"},
 | 
					      %{label: gettext("Date"), key: :date},
 | 
				
			||||||
      %{label: nil, key: "actions", sortable: false}
 | 
					      %{label: nil, key: :actions, sortable: false}
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rows =
 | 
					    rows =
 | 
				
			||||||
@@ -104,41 +107,43 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
  defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id),
 | 
					  defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id),
 | 
				
			||||||
    do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user))
 | 
					    do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec get_table_row_for_shot_group(AmmoGroup.t(), ShotGroup.t(), [map()]) :: [map()]
 | 
					  @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, %{date: date} = shot_group, columns) do
 | 
				
			||||||
    assigns = %{ammo_group: ammo_group, shot_group: shot_group}
 | 
					    assigns = %{ammo_group: ammo_group, shot_group: shot_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns
 | 
					    columns
 | 
				
			||||||
    |> Enum.into(%{}, fn %{key: key} ->
 | 
					    |> Map.new(fn %{key: key} ->
 | 
				
			||||||
      value =
 | 
					      value =
 | 
				
			||||||
        case key do
 | 
					        case key do
 | 
				
			||||||
          "date" ->
 | 
					          :date ->
 | 
				
			||||||
            {date, date |> display_date()}
 | 
					            {date, date |> display_date()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          "actions" ->
 | 
					          :actions ->
 | 
				
			||||||
            ~H"""
 | 
					            ~H"""
 | 
				
			||||||
            <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					            <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
				
			||||||
              <%= live_patch to: Routes.ammo_group_show_path(Endpoint, :edit_shot_group, @ammo_group, shot_group),
 | 
					              <.link
 | 
				
			||||||
                          class: "text-primary-600 link",
 | 
					                patch={Routes.ammo_group_show_path(Endpoint, :edit_shot_group, @ammo_group, @shot_group)}
 | 
				
			||||||
                          data: [qa: "edit-#{shot_group.id}"] do %>
 | 
					                class="text-primary-600 link"
 | 
				
			||||||
 | 
					                data-qa={"edit-#{@shot_group.id}"}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
                <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					                <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
              <% end %>
 | 
					              </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <%= link to: "#",
 | 
					              <.link
 | 
				
			||||||
                    class: "text-primary-600 link",
 | 
					                href="#"
 | 
				
			||||||
                    phx_click: "delete_shot_group",
 | 
					                class="text-primary-600 link"
 | 
				
			||||||
                    phx_value_id: shot_group.id,
 | 
					                phx-click="delete_shot_group"
 | 
				
			||||||
                    data: [
 | 
					                phx-value-id={@shot_group.id}
 | 
				
			||||||
                      confirm: dgettext("prompts", "Are you sure you want to delete this shot record?"),
 | 
					                data-confirm={dgettext("prompts", "Are you sure you want to delete this shot record?")}
 | 
				
			||||||
                      qa: "delete-#{shot_group.id}"
 | 
					                data-qa={"delete-#{@shot_group.id}"}
 | 
				
			||||||
                    ] do %>
 | 
					              >
 | 
				
			||||||
                <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					                <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
              <% end %>
 | 
					              </.link>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            """
 | 
					            """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          key ->
 | 
					          key ->
 | 
				
			||||||
            shot_group |> Map.get(key |> String.to_existing_atom())
 | 
					            shot_group |> Map.get(key)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {key, value}
 | 
					      {key, value}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,12 +11,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <span class="rounded-lg title text-lg">
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
      <%= gettext("Original count:") %>
 | 
					      <%= gettext("Original count:") %>
 | 
				
			||||||
      <%= @ammo_group.count + Ammo.get_used_count(@ammo_group) %>
 | 
					      <%= Ammo.get_original_count(@ammo_group) %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span class="rounded-lg title text-lg">
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
      <%= gettext("Percentage left:") %>
 | 
					      <%= gettext("Percentage left:") %>
 | 
				
			||||||
      <%= "#{@ammo_group |> Ammo.get_percentage_remaining()}%" %>
 | 
					      <%= gettext("%{percentage}%", percentage: @ammo_group |> Ammo.get_percentage_remaining()) %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @ammo_group.notes do %>
 | 
					    <%= if @ammo_group.notes do %>
 | 
				
			||||||
@@ -26,6 +26,11 @@
 | 
				
			|||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Purchased on:") %>
 | 
				
			||||||
 | 
					      <%= @ammo_group.purchased_on |> display_date() %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @ammo_group.price_paid do %>
 | 
					    <%= if @ammo_group.price_paid do %>
 | 
				
			||||||
      <span class="rounded-lg title text-lg">
 | 
					      <span class="rounded-lg title text-lg">
 | 
				
			||||||
        <%= gettext("Original cost:") %>
 | 
					        <%= gettext("Original cost:") %>
 | 
				
			||||||
@@ -47,46 +52,54 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-col justify-center items-center">
 | 
					  <div class="flex flex-col justify-center items-center">
 | 
				
			||||||
    <div class="flex flex-wrap justify-center items-center text-primary-600">
 | 
					    <div class="flex flex-wrap justify-center items-center text-primary-600">
 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Ammo Details"),
 | 
					      <.link
 | 
				
			||||||
        to: Routes.ammo_type_show_path(Endpoint, :show, @ammo_group.ammo_type),
 | 
					        navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_group.ammo_type)}
 | 
				
			||||||
        class: "mx-4 my-2 btn btn-primary",
 | 
					        class="mx-4 my-2 btn btn-primary"
 | 
				
			||||||
        data: [qa: "details"]
 | 
					        data-qa="details"
 | 
				
			||||||
      ) %>
 | 
					      >
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "View in Catalog") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= live_patch to: Routes.ammo_group_show_path(Endpoint, :edit, @ammo_group),
 | 
					      <.link
 | 
				
			||||||
                 class: "mx-4 my-2 text-primary-600 link",
 | 
					        patch={Routes.ammo_group_show_path(Endpoint, :edit, @ammo_group)}
 | 
				
			||||||
                 data: [qa: "edit"] do %>
 | 
					        class="mx-4 my-2 text-primary-600 link"
 | 
				
			||||||
 | 
					        data-qa="edit"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					        <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
      <% end %>
 | 
					      </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= link to: "#",
 | 
					      <.link
 | 
				
			||||||
           class: "mx-4 my-2 text-primary-600 link",
 | 
					        href="#"
 | 
				
			||||||
           phx_click: "delete",
 | 
					        class="mx-4 my-2 text-primary-600 link"
 | 
				
			||||||
           data: [
 | 
					        phx-click="delete"
 | 
				
			||||||
             confirm: dgettext("prompts", "Are you sure you want to delete this ammo?"),
 | 
					        data-confirm={dgettext("prompts", "Are you sure you want to delete this ammo?")}
 | 
				
			||||||
             qa: "delete"
 | 
					        data-qa="delete"
 | 
				
			||||||
           ] do %>
 | 
					      >
 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					        <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
      <% end %>
 | 
					      </.link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="flex flex-wrap justify-center items-center text-primary-600">
 | 
					    <div class="flex flex-wrap justify-center items-center text-primary-600">
 | 
				
			||||||
      <button type="button" class="mx-4 my-2 btn btn-primary" phx-click="toggle_staged">
 | 
					      <button type="button" class="mx-4 my-2 btn btn-primary" phx-click="toggle_staged">
 | 
				
			||||||
        <%= if @ammo_group.staged,
 | 
					        <%= if @ammo_group.staged,
 | 
				
			||||||
          do: gettext("Unstage from range"),
 | 
					          do: dgettext("actions", "Unstage from range"),
 | 
				
			||||||
          else: gettext("Stage for range") %>
 | 
					          else: dgettext("actions", "Stage for range") %>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Move containers"),
 | 
					      <.link
 | 
				
			||||||
        to: Routes.ammo_group_show_path(Endpoint, :move, @ammo_group),
 | 
					        patch={Routes.ammo_group_show_path(Endpoint, :move, @ammo_group)}
 | 
				
			||||||
        class: "btn btn-primary",
 | 
					        class="btn btn-primary"
 | 
				
			||||||
        data: [qa: "move"]
 | 
					        data-qa="move"
 | 
				
			||||||
      ) %>
 | 
					      >
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Move containers") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Record shots"),
 | 
					      <.link
 | 
				
			||||||
        to: Routes.ammo_group_show_path(Endpoint, :add_shot_group, @ammo_group),
 | 
					        patch={Routes.ammo_group_show_path(Endpoint, :add_shot_group, @ammo_group)}
 | 
				
			||||||
        class: "mx-4 my-2 btn btn-primary"
 | 
					        class="mx-4 my-2 btn btn-primary"
 | 
				
			||||||
      ) %>
 | 
					      >
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Record shots") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -100,7 +113,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <.container_card container={@ammo_group.container} />
 | 
					      <.container_card container={@ammo_group.container} />
 | 
				
			||||||
    <% else %>
 | 
					    <% else %>
 | 
				
			||||||
      <%= gettext("This ammo group is not in a container") %>
 | 
					      <%= gettext("This ammo is not in a container") %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,17 +13,13 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
 | 
				
			|||||||
          %{:ammo_type => AmmoType.t(), :current_user => User.t(), optional(any) => any},
 | 
					          %{:ammo_type => AmmoType.t(), :current_user => User.t(), optional(any) => any},
 | 
				
			||||||
          Socket.t()
 | 
					          Socket.t()
 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{ammo_type: ammo_type, current_user: _current_user} = assigns, socket) do
 | 
					  def update(%{current_user: _current_user} = assigns, socket) do
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:changeset, Ammo.change_ammo_type(ammo_type))}
 | 
					    {:ok, socket |> assign(assigns) |> assign_changeset(%{})}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event("validate", %{"ammo_type" => ammo_type_params}, socket) do
 | 
				
			||||||
        "validate",
 | 
					    {:noreply, socket |> assign_changeset(ammo_type_params)}
 | 
				
			||||||
        %{"ammo_type" => ammo_type_params},
 | 
					 | 
				
			||||||
        %{assigns: %{ammo_type: ammo_type}} = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, ammo_type |> Ammo.change_ammo_type(ammo_type_params))}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
@@ -34,6 +30,31 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
 | 
				
			|||||||
    save_ammo_type(socket, action, ammo_type_params)
 | 
					    save_ammo_type(socket, action, ammo_type_params)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp assign_changeset(
 | 
				
			||||||
 | 
					         %{assigns: %{action: action, ammo_type: ammo_type, current_user: user}} = socket,
 | 
				
			||||||
 | 
					         ammo_type_params
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    changeset_action =
 | 
				
			||||||
 | 
					      cond do
 | 
				
			||||||
 | 
					        action in [:new, :clone] -> :insert
 | 
				
			||||||
 | 
					        action == :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)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case changeset |> Changeset.apply_action(changeset_action) do
 | 
				
			||||||
 | 
					        {:ok, _data} -> changeset
 | 
				
			||||||
 | 
					        {:error, changeset} -> changeset
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(changeset: changeset)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp save_ammo_type(
 | 
					  defp save_ammo_type(
 | 
				
			||||||
         %{assigns: %{ammo_type: ammo_type, current_user: current_user, return_to: return_to}} =
 | 
					         %{assigns: %{ammo_type: ammo_type, current_user: current_user, return_to: return_to}} =
 | 
				
			||||||
           socket,
 | 
					           socket,
 | 
				
			||||||
@@ -44,7 +65,7 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
 | 
				
			|||||||
      case Ammo.update_ammo_type(ammo_type, ammo_type_params, current_user) do
 | 
					      case Ammo.update_ammo_type(ammo_type, ammo_type_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: ammo_type_name}} ->
 | 
					        {:ok, %{name: ammo_type_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} updated successfully", name: ammo_type_name)
 | 
					          prompt = dgettext("prompts", "%{name} updated successfully", name: ammo_type_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
@@ -55,14 +76,15 @@ defmodule CanneryWeb.AmmoTypeLive.FormComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  defp save_ammo_type(
 | 
					  defp save_ammo_type(
 | 
				
			||||||
         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
					         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
				
			||||||
         :new,
 | 
					         action,
 | 
				
			||||||
         ammo_type_params
 | 
					         ammo_type_params
 | 
				
			||||||
       ) do
 | 
					       )
 | 
				
			||||||
 | 
					       when action in [:new, :clone] do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      case Ammo.create_ammo_type(ammo_type_params, current_user) do
 | 
					      case Ammo.create_ammo_type(ammo_type_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: ammo_type_name}} ->
 | 
					        {:ok, %{name: ammo_type_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} created successfully", name: ammo_type_name)
 | 
					          prompt = dgettext("prompts", "%{name} created successfully", name: ammo_type_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
    <%= @title %>
 | 
					    <%= @title %>
 | 
				
			||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="ammo_type-form"
 | 
					    id="ammo_type-form"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,15 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
 | 
					 | 
				
			||||||
  alias Cannery.{Ammo, Ammo.AmmoType}
 | 
					  alias Cannery.{Ammo, Ammo.AmmoType}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(%{"search" => search}, _session, socket) do
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session) |> list_ammo_types()}
 | 
					    {:ok, socket |> assign(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()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -19,19 +21,49 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
				
			||||||
 | 
					    %{name: ammo_type_name} = ammo_type = Ammo.get_ammo_type!(id, current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Edit Ammo type"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_type, Ammo.get_ammo_type!(id, current_user))
 | 
					      page_title: gettext("Edit %{ammo_type_name}", ammo_type_name: ammo_type_name),
 | 
				
			||||||
 | 
					      ammo_type: ammo_type
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :clone, %{"id" => id}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("New Ammo type"),
 | 
				
			||||||
 | 
					      ammo_type: %{Ammo.get_ammo_type!(id, current_user) | id: nil}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :new, _params) do
 | 
					  defp apply_action(socket, :new, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("New Ammo type"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_type, %AmmoType{})
 | 
					      page_title: gettext("New Ammo type"),
 | 
				
			||||||
 | 
					      ammo_type: %AmmoType{}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket |> assign(:page_title, gettext("Ammo types")) |> assign(:ammo_type, nil)
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Catalog"),
 | 
				
			||||||
 | 
					      search: nil,
 | 
				
			||||||
 | 
					      ammo_type: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> list_ammo_types()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(socket, :search, %{"search" => search}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Catalog"),
 | 
				
			||||||
 | 
					      search: search,
 | 
				
			||||||
 | 
					      ammo_type: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> list_ammo_types()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -43,111 +75,22 @@ defmodule CanneryWeb.AmmoTypeLive.Index do
 | 
				
			|||||||
    {:noreply, socket |> put_flash(:info, prompt) |> list_ammo_types()}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> list_ammo_types()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp list_ammo_types(%{assigns: %{current_user: current_user}} = socket) do
 | 
					  @impl true
 | 
				
			||||||
    ammo_types = Ammo.list_ammo_types(current_user)
 | 
					  def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> assign(:show_used, !show_used) |> list_ammo_types()}
 | 
				
			||||||
    columns =
 | 
					 | 
				
			||||||
      [
 | 
					 | 
				
			||||||
        %{label: gettext("Name"), key: "name", type: :string},
 | 
					 | 
				
			||||||
        %{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}
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
      |> Enum.filter(fn %{key: key, type: type} ->
 | 
					 | 
				
			||||||
        # remove columns if all values match defaults
 | 
					 | 
				
			||||||
        default_value = if type == :boolean, do: false, else: nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ammo_types
 | 
					 | 
				
			||||||
        |> Enum.any?(fn ammo_type ->
 | 
					 | 
				
			||||||
          not (ammo_type |> Map.get(key |> String.to_existing_atom()) == default_value)
 | 
					 | 
				
			||||||
        end)
 | 
					 | 
				
			||||||
      end)
 | 
					 | 
				
			||||||
      |> Kernel.++([
 | 
					 | 
				
			||||||
        %{label: gettext("Total # of rounds"), key: "round_count", type: :round_count},
 | 
					 | 
				
			||||||
        %{label: gettext("Average Price paid"), key: "avg_price_paid", type: :avg_price_paid},
 | 
					 | 
				
			||||||
        %{label: nil, key: "actions", type: :actions, sortable: false}
 | 
					 | 
				
			||||||
      ])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rows =
 | 
					 | 
				
			||||||
      ammo_types
 | 
					 | 
				
			||||||
      |> Enum.map(fn ammo_type -> ammo_type |> get_ammo_type_values(columns, current_user) end)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    socket |> assign(columns: columns, rows: rows)
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp get_ammo_type_values(ammo_type, columns, current_user) do
 | 
					  @impl true
 | 
				
			||||||
    columns
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
    |> Enum.into(%{}, fn %{key: key, type: type} ->
 | 
					    {:noreply, socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :index))}
 | 
				
			||||||
      {key, get_ammo_type_value(type, key, ammo_type, current_user)}
 | 
					 | 
				
			||||||
    end)
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp get_ammo_type_value(:boolean, key, ammo_type, _current_user),
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
    do: ammo_type |> Map.get(key |> String.to_existing_atom()) |> humanize()
 | 
					    {:noreply,
 | 
				
			||||||
 | 
					     socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :search, search_term))}
 | 
				
			||||||
  defp get_ammo_type_value(:round_count, _key, ammo_type, current_user),
 | 
					 | 
				
			||||||
    do: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_ammo_type_value(:avg_price_paid, _key, ammo_type, 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))
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp get_ammo_type_value(:actions, _key, ammo_type, _current_user) do
 | 
					  defp list_ammo_types(%{assigns: %{search: search, current_user: current_user}} = socket) do
 | 
				
			||||||
    assigns = %{ammo_type: ammo_type}
 | 
					    socket |> assign(ammo_types: Ammo.list_ammo_types(search, current_user))
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
      <%= live_redirect to: Routes.ammo_type_show_path(Endpoint, :show, ammo_type),
 | 
					 | 
				
			||||||
                    class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                    data: [qa: "view-#{ammo_type.id}"] do %>
 | 
					 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= live_patch to: Routes.ammo_type_index_path(Endpoint, :edit, ammo_type),
 | 
					 | 
				
			||||||
                  class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                  data: [qa: "edit-#{ammo_type.id}"] do %>
 | 
					 | 
				
			||||||
        <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= link to: "#",
 | 
					 | 
				
			||||||
            class: "text-primary-600 link",
 | 
					 | 
				
			||||||
            phx_click: "delete",
 | 
					 | 
				
			||||||
            phx_value_id: ammo_type.id,
 | 
					 | 
				
			||||||
            data: [
 | 
					 | 
				
			||||||
              confirm: dgettext("prompts", "Are you sure you want to delete this ammo?"),
 | 
					 | 
				
			||||||
              qa: "delete-#{ammo_type.id}"
 | 
					 | 
				
			||||||
            ] do %>
 | 
					 | 
				
			||||||
        <i class="fa-lg fas fa-trash"></i>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_ammo_type_value(nil, _key, _ammo_type, _current_user), do: nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp get_ammo_type_value(_other, key, ammo_type, _current_user),
 | 
					 | 
				
			||||||
    do: ammo_type |> Map.get(key |> String.to_existing_atom())
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,110 @@
 | 
				
			|||||||
<div class="flex flex-col space-y-8 justify-center items-center">
 | 
					<div class="flex flex-col space-y-8 justify-center items-center">
 | 
				
			||||||
  <h1 class="title text-2xl title-primary-500">
 | 
					  <h1 class="title text-2xl title-primary-500">
 | 
				
			||||||
    <%= gettext("Ammo Types") %>
 | 
					    <%= gettext("Catalog") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= if @rows |> Enum.empty?() do %>
 | 
					  <%= if @ammo_types |> Enum.empty?() and @search |> is_nil() do %>
 | 
				
			||||||
    <h2 class="title text-xl text-primary-600">
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
      <%= gettext("No Ammo Types") %>
 | 
					      <%= gettext("No Ammo types") %>
 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h2>
 | 
					    </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Add your first type!"),
 | 
					    <.link patch={Routes.ammo_type_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.ammo_type_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "Add your first type!") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "New Ammo type"),
 | 
					    <.link patch={Routes.ammo_type_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.ammo_type_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "New Ammo type") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <.live_component
 | 
					    <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">
 | 
				
			||||||
      module={CanneryWeb.Components.TableComponent}
 | 
					      <.form
 | 
				
			||||||
      id="ammo_types_index_table"
 | 
					        :let={f}
 | 
				
			||||||
      action={@live_action}
 | 
					        for={:search}
 | 
				
			||||||
      columns={@columns}
 | 
					        phx-change="search"
 | 
				
			||||||
      rows={@rows}
 | 
					        phx-submit="search"
 | 
				
			||||||
    />
 | 
					        class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
 | 
					        data-qa="ammo_type_search"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					          class: "input input-primary",
 | 
				
			||||||
 | 
					          value: @search,
 | 
				
			||||||
 | 
					          phx_debounce: 300,
 | 
				
			||||||
 | 
					          placeholder: gettext("Search catalog")
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					      </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <.toggle_button action="toggle_show_used" value={@show_used}>
 | 
				
			||||||
 | 
					        <span class="title text-lg text-primary-600">
 | 
				
			||||||
 | 
					          <%= gettext("Show used") %>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </.toggle_button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= if @ammo_types |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					      <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("No Ammo types") %>
 | 
				
			||||||
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.AmmoTypeTableComponent}
 | 
				
			||||||
 | 
					        id="ammo_types_index_table"
 | 
				
			||||||
 | 
					        action={@live_action}
 | 
				
			||||||
 | 
					        ammo_types={@ammo_types}
 | 
				
			||||||
 | 
					        current_user={@current_user}
 | 
				
			||||||
 | 
					        show_used={@show_used}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <: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}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.ammo_type_index_path(Endpoint, :edit, ammo_type)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"edit-#{ammo_type.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.ammo_type_index_path(Endpoint, :clone, ammo_type)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"clone-#{ammo_type.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              href="#"
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              phx-click="delete"
 | 
				
			||||||
 | 
					              phx-value-id={ammo_type.id}
 | 
				
			||||||
 | 
					              data-confirm={
 | 
				
			||||||
 | 
					                dgettext(
 | 
				
			||||||
 | 
					                  "prompts",
 | 
				
			||||||
 | 
					                  "Are you sure you want to delete %{name}? This will delete all %{name} type ammo as well!",
 | 
				
			||||||
 | 
					                  name: ammo_type.name
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              data-qa={"delete-#{ammo_type.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </:actions>
 | 
				
			||||||
 | 
					      </.live_component>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action in [:new, :edit] do %>
 | 
					<%= if @live_action in [:new, :edit, :clone] do %>
 | 
				
			||||||
  <.modal return_to={Routes.ammo_type_index_path(Endpoint, :index)}>
 | 
					  <.modal return_to={Routes.ammo_type_index_path(Endpoint, :index)}>
 | 
				
			||||||
    <.live_component
 | 
					    <.live_component
 | 
				
			||||||
      module={CanneryWeb.AmmoTypeLive.FormComponent}
 | 
					      module={CanneryWeb.AmmoTypeLive.FormComponent}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,26 +5,41 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  import CanneryWeb.Components.AmmoGroupCard
 | 
					  import CanneryWeb.Components.AmmoGroupCard
 | 
				
			||||||
  alias Cannery.Ammo
 | 
					  alias Cannery.{Ammo, Ammo.AmmoType}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @fields_list [
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					    %{label: gettext("Bullet type:"), key: :bullet_type, type: :string},
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session)}
 | 
					    %{label: gettext("Bullet core:"), key: :bullet_core, type: :string},
 | 
				
			||||||
  end
 | 
					    %{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
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(%{"id" => id}, _params, %{assigns: %{current_user: current_user}} = socket) do
 | 
					  def mount(_params, _session, %{assigns: %{live_action: live_action}} = socket),
 | 
				
			||||||
    ammo_type = Ammo.get_ammo_type!(id, current_user)
 | 
					    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 =
 | 
				
			||||||
      socket
 | 
					      socket
 | 
				
			||||||
      |> assign(
 | 
					      |> assign(view_table: live_action == :table)
 | 
				
			||||||
        page_title: page_title(socket.assigns.live_action),
 | 
					      |> display_ammo_type(id)
 | 
				
			||||||
        ammo_type: ammo_type,
 | 
					 | 
				
			||||||
        ammo_groups: ammo_type |> Ammo.list_ammo_groups_for_type(current_user),
 | 
					 | 
				
			||||||
        avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user)
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -40,9 +55,67 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
 | 
				
			|||||||
    prompt = dgettext("prompts", "%{name} deleted succesfully", name: ammo_type_name)
 | 
					    prompt = dgettext("prompts", "%{name} deleted succesfully", name: ammo_type_name)
 | 
				
			||||||
    redirect_to = Routes.ammo_type_index_path(socket, :index)
 | 
					    redirect_to = Routes.ammo_type_index_path(socket, :index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt) |> push_redirect(to: redirect_to)}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp page_title(:show), do: gettext("Show Ammo type")
 | 
					  @impl true
 | 
				
			||||||
  defp page_title(:edit), do: gettext("Edit Ammo type")
 | 
					  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)}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_type(
 | 
				
			||||||
 | 
					         %{assigns: %{live_action: live_action, current_user: current_user, show_used: show_used}} =
 | 
				
			||||||
 | 
					           socket,
 | 
				
			||||||
 | 
					         %AmmoType{} = ammo_type
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    fields_to_display =
 | 
				
			||||||
 | 
					      @fields_list
 | 
				
			||||||
 | 
					      |> Enum.any?(fn %{key: field, type: type} ->
 | 
				
			||||||
 | 
					        default_value =
 | 
				
			||||||
 | 
					          case type do
 | 
				
			||||||
 | 
					            :boolean -> false
 | 
				
			||||||
 | 
					            _other_type -> nil
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ammo_type |> Map.get(field) != default_value
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: page_title(live_action, ammo_type),
 | 
				
			||||||
 | 
					      ammo_type: ammo_type,
 | 
				
			||||||
 | 
					      ammo_groups: ammo_type |> Ammo.list_ammo_groups_for_type(current_user, show_used),
 | 
				
			||||||
 | 
					      avg_cost_per_round: ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user),
 | 
				
			||||||
 | 
					      fields_list: @fields_list,
 | 
				
			||||||
 | 
					      fields_to_display: fields_to_display
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_type(%{assigns: %{current_user: current_user}} = socket, ammo_type_id) do
 | 
				
			||||||
 | 
					    socket |> display_ammo_type(Ammo.get_ammo_type!(ammo_type_id, current_user))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_type(%{assigns: %{ammo_type: ammo_type}} = socket) 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 page_title(:edit, %{name: ammo_type_name}),
 | 
				
			||||||
 | 
					    do: gettext("Edit %{ammo_type_name}", ammo_type_name: ammo_type_name)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
<div class="mx-auto px-4 sm:px-8 space-y-4 max-w-3xl
 | 
					<div class="space-y-4 flex flex-col justify-center items-center">
 | 
				
			||||||
  flex flex-col justify-center items-center">
 | 
					 | 
				
			||||||
  <h1 class="title text-2xl title-primary-500">
 | 
					  <h1 class="title text-2xl title-primary-500">
 | 
				
			||||||
    <%= @ammo_type.name %>
 | 
					    <%= @ammo_type.name %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
@@ -13,66 +12,59 @@
 | 
				
			|||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex space-x-4 justify-center items-center text-primary-600">
 | 
					  <div class="flex space-x-4 justify-center items-center text-primary-600">
 | 
				
			||||||
    <%= live_patch to: Routes.ammo_type_show_path(Endpoint, :edit, @ammo_type),
 | 
					    <.link
 | 
				
			||||||
               class: "text-primary-600 link",
 | 
					      patch={Routes.ammo_type_show_path(Endpoint, :edit, @ammo_type)}
 | 
				
			||||||
               data: [qa: "edit"] do %>
 | 
					      class="text-primary-600 link"
 | 
				
			||||||
 | 
					      data-qa="edit"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					      <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
    <% end %>
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= link to: "#",
 | 
					    <.link
 | 
				
			||||||
         class: "text-primary-600 link",
 | 
					      href="#"
 | 
				
			||||||
         phx_click: "delete",
 | 
					      class="text-primary-600 link"
 | 
				
			||||||
         data: [
 | 
					      phx-click="delete"
 | 
				
			||||||
           confirm:
 | 
					      data-confirm={
 | 
				
			||||||
             dgettext("prompts", "Are you sure you want to delete %{name}?", name: @ammo_type.name),
 | 
					        dgettext(
 | 
				
			||||||
           qa: "delete"
 | 
					          "prompts",
 | 
				
			||||||
         ] do %>
 | 
					          "Are you sure you want to delete %{name}? This will delete all %{name} type ammo as well!",
 | 
				
			||||||
 | 
					          name: @ammo_type.name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      data-qa="delete"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					      <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
    <% end %>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
 | 
					  <%= if @fields_to_display do %>
 | 
				
			||||||
    <%= for {field_name, field, type} <- [
 | 
					    <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
 | 
				
			||||||
          {gettext("Bullet type"), :bullet_type, :string},
 | 
					      <%= for %{label: label, key: key, type: type} <- @fields_list do %>
 | 
				
			||||||
          {gettext("Bullet core"), :bullet_core, :string},
 | 
					        <%= if @ammo_type |> Map.get(key) do %>
 | 
				
			||||||
          {gettext("Cartridge"), :cartridge, :string},
 | 
					          <h3 class="title text-lg">
 | 
				
			||||||
          {gettext("Caliber"), :caliber, :string},
 | 
					            <%= label %>
 | 
				
			||||||
          {gettext("Case material"), :case_material, :string},
 | 
					          </h3>
 | 
				
			||||||
          {gettext("Jacket type"), :jacket_type, :string},
 | 
					 | 
				
			||||||
          {gettext("Muzzle velocity"), :muzzle_velocity, :string},
 | 
					 | 
				
			||||||
          {gettext("Powder type"), :powder_type, :string},
 | 
					 | 
				
			||||||
          {gettext("Powder grains per charge"), :powder_grains_per_charge, :string},
 | 
					 | 
				
			||||||
          {gettext("Grains"), :grains, :string},
 | 
					 | 
				
			||||||
          {gettext("Pressure"), :pressure, :string},
 | 
					 | 
				
			||||||
          {gettext("Primer type"), :primer_type, :string},
 | 
					 | 
				
			||||||
          {gettext("Firing type"), :firing_type, :string},
 | 
					 | 
				
			||||||
          {gettext("Tracer"), :tracer, :boolean},
 | 
					 | 
				
			||||||
          {gettext("Incendiary"), :incendiary, :boolean},
 | 
					 | 
				
			||||||
          {gettext("Blank"), :blank, :boolean},
 | 
					 | 
				
			||||||
          {gettext("Corrosive"), :corrosive, :boolean},
 | 
					 | 
				
			||||||
          {gettext("Manufacturer"), :manufacturer, :string},
 | 
					 | 
				
			||||||
          {gettext("UPC"), :upc, :string}
 | 
					 | 
				
			||||||
        ] do %>
 | 
					 | 
				
			||||||
      <%= if @ammo_type |> Map.get(field) do %>
 | 
					 | 
				
			||||||
        <h3 class="title text-lg">
 | 
					 | 
				
			||||||
          <%= field_name %>:
 | 
					 | 
				
			||||||
        </h3>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span class="text-primary-600">
 | 
					          <span class="text-primary-600">
 | 
				
			||||||
          <%= case type do %>
 | 
					            <%= case type do %>
 | 
				
			||||||
            <% :boolean -> %>
 | 
					              <% :boolean -> %>
 | 
				
			||||||
              <%= @ammo_type |> Map.get(field) |> humanize() %>
 | 
					                <%= @ammo_type |> Map.get(key) |> humanize() %>
 | 
				
			||||||
            <% _ -> %>
 | 
					              <% _ -> %>
 | 
				
			||||||
              <%= @ammo_type |> Map.get(field) %>
 | 
					                <%= @ammo_type |> Map.get(key) %>
 | 
				
			||||||
          <% end %>
 | 
					            <% end %>
 | 
				
			||||||
        </span>
 | 
					          </span>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    <% end %>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <hr class="hr" />
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
 | 
				
			||||||
    <h3 class="title text-lg">
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
      <%= gettext("Current # of rounds:") %>
 | 
					      <%= gettext("Rounds:") %>
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span class="text-primary-600">
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
@@ -80,16 +72,64 @@
 | 
				
			|||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3 class="title text-lg">
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
      <%= gettext("Total rounds shot:") %>
 | 
					      <%= gettext("Used rounds:") %>
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span class="text-primary-600">
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
      <%= @ammo_type |> Ammo.get_used_count_for_ammo_type(@current_user) %>
 | 
					      <%= @ammo_type |> Ammo.get_used_count_for_ammo_type(@current_user) %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Total ever rounds:") %>
 | 
				
			||||||
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
 | 
					      <%= @ammo_type |> Ammo.get_historical_count_for_ammo_type(@current_user) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <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) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <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) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Total ever packs:") %>
 | 
				
			||||||
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
 | 
					      <%= @ammo_type |> Ammo.get_ammo_groups_count_for_type(@current_user, true) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <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">
 | 
				
			||||||
 | 
					      <%= @ammo_type.inserted_at |> display_datetime() %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @avg_cost_per_round do %>
 | 
					    <%= if @avg_cost_per_round do %>
 | 
				
			||||||
      <h3 class="title text-lg">
 | 
					      <h3 class="title text-lg">
 | 
				
			||||||
        <%= gettext("Average Price paid") %>:
 | 
					        <%= gettext("Average CPR") %>:
 | 
				
			||||||
      </h3>
 | 
					      </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <span class="text-primary-600">
 | 
					      <span class="text-primary-600">
 | 
				
			||||||
@@ -106,18 +146,50 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div>
 | 
					  <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.toggle_button action="toggle_table" value={@view_table}>
 | 
				
			||||||
 | 
					      <span class="title text-lg text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("View as table") %>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    </.toggle_button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="w-full p-4">
 | 
				
			||||||
    <%= if @ammo_groups |> Enum.empty?() do %>
 | 
					    <%= if @ammo_groups |> Enum.empty?() do %>
 | 
				
			||||||
      <h2 class="mx-8 my-4 title text-lg text-primary-600">
 | 
					      <h2 class="px-4 title text-lg text-primary-600">
 | 
				
			||||||
        <%= gettext("No ammo for this type") %>
 | 
					        <%= gettext("No ammo for this type") %>
 | 
				
			||||||
        <%= display_emoji("😔") %>
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
    <% else %>
 | 
					    <% else %>
 | 
				
			||||||
      <div class="flex flex-wrap justify-center items-center">
 | 
					      <%= if @view_table do %>
 | 
				
			||||||
        <%= for ammo_group <- @ammo_groups do %>
 | 
					        <.live_component
 | 
				
			||||||
          <.ammo_group_card ammo_group={ammo_group} />
 | 
					          module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
        <% end %>
 | 
					          id="ammo-type-show-table"
 | 
				
			||||||
      </div>
 | 
					          ammo_groups={@ammo_groups}
 | 
				
			||||||
 | 
					          current_user={@current_user}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <:container :let={%{container: %{name: container_name} = container}}>
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              navigate={Routes.container_show_path(Endpoint, :show, container)}
 | 
				
			||||||
 | 
					              class="mx-2 my-1 link"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <%= container_name %>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </:container>
 | 
				
			||||||
 | 
					        </.live_component>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <div class="flex flex-wrap justify-center items-stretch">
 | 
				
			||||||
 | 
					          <%= for ammo_group <- @ammo_groups do %>
 | 
				
			||||||
 | 
					            <.ammo_group_card ammo_group={ammo_group} show_container={true} />
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,24 +5,25 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-wrap justify-center items-center">
 | 
					  <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
    <%= for tag <- @container.tags do %>
 | 
					    <%= for tag <- @container.tags do %>
 | 
				
			||||||
      <%= link to: "#",
 | 
					      <.link
 | 
				
			||||||
            class: "mx-2 my-1 px-4 py-2 rounded-lg title text-xl",
 | 
					        href="#"
 | 
				
			||||||
            style: "color: #{tag.text_color}; background-color: #{tag.bg_color}",
 | 
					        class="mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
 | 
				
			||||||
            phx_click: "delete",
 | 
					        style={"color: #{tag.text_color}; background-color: #{tag.bg_color}"}
 | 
				
			||||||
            phx_value_tag_id: tag.id,
 | 
					        phx-click="delete"
 | 
				
			||||||
            phx_target: @myself,
 | 
					        phx-value-tag-id={tag.id}
 | 
				
			||||||
            data: [
 | 
					        phx-target={@myself}
 | 
				
			||||||
              confirm:
 | 
					        data-confirm={
 | 
				
			||||||
                dgettext(
 | 
					          dgettext(
 | 
				
			||||||
                  "prompts",
 | 
					            "prompts",
 | 
				
			||||||
                  "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
 | 
					            "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
 | 
				
			||||||
                  tag_name: tag.name,
 | 
					            tag_name: tag.name,
 | 
				
			||||||
                  container_name: @container.name
 | 
					            container_name: @container.name
 | 
				
			||||||
                )
 | 
					          )
 | 
				
			||||||
            ] do %>
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <%= tag.name %>
 | 
					        <%= tag.name %>
 | 
				
			||||||
        <i class="fa-fw fa-sm fas fa-trash"></i>
 | 
					        <i class="fa-fw fa-sm fas fa-trash"></i>
 | 
				
			||||||
      <% end %>
 | 
					      </.link>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @container.tags |> Enum.empty?() do %>
 | 
					    <%= if @container.tags |> Enum.empty?() do %>
 | 
				
			||||||
@@ -37,7 +38,7 @@
 | 
				
			|||||||
    <hr class="hr" />
 | 
					    <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <.form
 | 
					    <.form
 | 
				
			||||||
      let={f}
 | 
					      :let={f}
 | 
				
			||||||
      for={:tag}
 | 
					      for={:tag}
 | 
				
			||||||
      id="add-tag-to-container-form"
 | 
					      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"
 | 
					      class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,18 +13,13 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
 | 
				
			|||||||
          %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
 | 
					          %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
 | 
				
			||||||
          Socket.t()
 | 
					          Socket.t()
 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{container: container} = assigns, socket) do
 | 
					  def update(%{container: _container} = assigns, socket) do
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:changeset, Containers.change_container(container))}
 | 
					    {:ok, socket |> assign(assigns) |> assign_changeset(%{})}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event("validate", %{"container" => container_params}, socket) do
 | 
				
			||||||
        "validate",
 | 
					    {:noreply, socket |> assign_changeset(container_params)}
 | 
				
			||||||
        %{"container" => container_params},
 | 
					 | 
				
			||||||
        %{assigns: %{container: container}} = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    changeset = container |> Containers.change_container(container_params)
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, changeset)}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
@@ -35,6 +30,34 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
 | 
				
			|||||||
    save_container(socket, action, container_params)
 | 
					    save_container(socket, action, container_params)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp assign_changeset(
 | 
				
			||||||
 | 
					         %{assigns: %{action: action, container: container, current_user: user}} = socket,
 | 
				
			||||||
 | 
					         container_params
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    changeset_action =
 | 
				
			||||||
 | 
					      cond do
 | 
				
			||||||
 | 
					        action in [:new, :clone] -> :insert
 | 
				
			||||||
 | 
					        action == :edit -> :update
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      cond do
 | 
				
			||||||
 | 
					        action in [:new, :clone] ->
 | 
				
			||||||
 | 
					          container |> Container.create_changeset(user, container_params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        action == :edit ->
 | 
				
			||||||
 | 
					          container |> Container.update_changeset(container_params)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case changeset |> Changeset.apply_action(changeset_action) do
 | 
				
			||||||
 | 
					        {:ok, _data} -> changeset
 | 
				
			||||||
 | 
					        {:error, changeset} -> changeset
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(:changeset, changeset)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp save_container(
 | 
					  defp save_container(
 | 
				
			||||||
         %{assigns: %{container: container, current_user: current_user, return_to: return_to}} =
 | 
					         %{assigns: %{container: container, current_user: current_user, return_to: return_to}} =
 | 
				
			||||||
           socket,
 | 
					           socket,
 | 
				
			||||||
@@ -45,7 +68,7 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
 | 
				
			|||||||
      case Containers.update_container(container, current_user, container_params) do
 | 
					      case Containers.update_container(container, current_user, container_params) do
 | 
				
			||||||
        {:ok, %{name: container_name}} ->
 | 
					        {:ok, %{name: container_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} updated successfully", name: container_name)
 | 
					          prompt = dgettext("prompts", "%{name} updated successfully", name: container_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
@@ -56,14 +79,15 @@ defmodule CanneryWeb.ContainerLive.FormComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  defp save_container(
 | 
					  defp save_container(
 | 
				
			||||||
         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
					         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
				
			||||||
         :new,
 | 
					         action,
 | 
				
			||||||
         container_params
 | 
					         container_params
 | 
				
			||||||
       ) do
 | 
					       )
 | 
				
			||||||
 | 
					       when action in [:new, :clone] do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      case Containers.create_container(container_params, current_user) do
 | 
					      case Containers.create_container(container_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: container_name}} ->
 | 
					        {:ok, %{name: container_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} created successfully", name: container_name)
 | 
					          prompt = dgettext("prompts", "%{name} created successfully", name: container_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
    <%= @title %>
 | 
					    <%= @title %>
 | 
				
			||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="container-form"
 | 
					    id="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"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,12 +6,15 @@ defmodule CanneryWeb.ContainerLive.Index do
 | 
				
			|||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  import CanneryWeb.Components.ContainerCard
 | 
					  import CanneryWeb.Components.ContainerCard
 | 
				
			||||||
  alias Cannery.{Containers, Containers.Container, Repo}
 | 
					  alias Cannery.{Containers, Containers.Container, Repo}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					 | 
				
			||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(%{"search" => search}, _session, socket) do
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session)}
 | 
					    {:ok, socket |> assign(view_table: true, search: search) |> display_containers()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def mount(_params, _session, socket) do
 | 
				
			||||||
 | 
					    {:ok, socket |> assign(view_table: true, search: nil) |> display_containers()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -23,7 +26,7 @@ defmodule CanneryWeb.ContainerLive.Index do
 | 
				
			|||||||
    %{name: container_name} =
 | 
					    %{name: container_name} =
 | 
				
			||||||
      container =
 | 
					      container =
 | 
				
			||||||
      Containers.get_container!(id, current_user)
 | 
					      Containers.get_container!(id, current_user)
 | 
				
			||||||
      |> Repo.preload([:tags, :ammo_groups], force: true)
 | 
					      |> Repo.preload([:tags, :ammo_groups])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
 | 
					    |> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
 | 
				
			||||||
@@ -33,11 +36,29 @@ defmodule CanneryWeb.ContainerLive.Index do
 | 
				
			|||||||
    socket |> assign(:page_title, gettext("New Container")) |> assign(:container, %Container{})
 | 
					    socket |> assign(:page_title, gettext("New Container")) |> assign(:container, %Container{})
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :clone, %{"id" => id}) do
 | 
				
			||||||
 | 
					    container = Containers.get_container!(id, current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(page_title: gettext("New Container"), container: %{container | id: nil})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Containers"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:container, nil)
 | 
					      page_title: gettext("Containers"),
 | 
				
			||||||
    |> display_containers()
 | 
					      container: nil,
 | 
				
			||||||
 | 
					      search: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(socket, :search, %{"search" => search}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Containers"),
 | 
				
			||||||
 | 
					      container: nil,
 | 
				
			||||||
 | 
					      search: search
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
 | 
				
			||||||
@@ -85,10 +106,26 @@ defmodule CanneryWeb.ContainerLive.Index do
 | 
				
			|||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp display_containers(%{assigns: %{current_user: current_user}} = socket) do
 | 
					  @impl true
 | 
				
			||||||
    containers =
 | 
					  def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
 | 
				
			||||||
      Containers.list_containers(current_user) |> Repo.preload([:tags, :ammo_groups], force: true)
 | 
					    {:noreply, socket |> assign(:view_table, !view_table) |> display_containers()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket |> assign(containers: containers)
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.container_index_path(Endpoint, :index))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply,
 | 
				
			||||||
 | 
					     socket |> push_patch(to: Routes.container_index_path(Endpoint, :search, search_term))}
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,57 +3,152 @@
 | 
				
			|||||||
    <%= gettext("Containers") %>
 | 
					    <%= gettext("Containers") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= if @containers |> Enum.empty?() and @search |> is_nil() do %>
 | 
				
			||||||
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					      <%= gettext("No containers") %>
 | 
				
			||||||
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					      <%= dgettext("actions", "Add your first container!") %>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					  <% else %>
 | 
				
			||||||
 | 
					    <.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					      <%= 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">
 | 
				
			||||||
 | 
					      <.form
 | 
				
			||||||
 | 
					        :let={f}
 | 
				
			||||||
 | 
					        for={:search}
 | 
				
			||||||
 | 
					        phx-change="search"
 | 
				
			||||||
 | 
					        phx-submit="search"
 | 
				
			||||||
 | 
					        class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
 | 
					        data-qa="container_search"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					          class: "input input-primary",
 | 
				
			||||||
 | 
					          value: @search,
 | 
				
			||||||
 | 
					          phx_debounce: 300,
 | 
				
			||||||
 | 
					          placeholder: gettext("Search containers")
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					      </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <.toggle_button action="toggle_table" value={@view_table}>
 | 
				
			||||||
 | 
					        <span class="title text-lg text-primary-600">
 | 
				
			||||||
 | 
					          <%= gettext("View as table") %>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </.toggle_button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= if @containers |> Enum.empty?() do %>
 | 
					  <%= if @containers |> Enum.empty?() do %>
 | 
				
			||||||
    <h2 class="title text-xl text-primary-600">
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
      <%= gettext("No containers") %>
 | 
					      <%= gettext("No containers") %>
 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h2>
 | 
					    </h2>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Add your first container!"),
 | 
					 | 
				
			||||||
      to: Routes.container_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
      class: "btn btn-primary"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "New Container"),
 | 
					    <%= if @view_table do %>
 | 
				
			||||||
      to: Routes.container_index_path(Endpoint, :new),
 | 
					      <.live_component
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					        module={CanneryWeb.Components.ContainerTableComponent}
 | 
				
			||||||
    ) %>
 | 
					        id="containers_index_table"
 | 
				
			||||||
  <% end %>
 | 
					        action={@live_action}
 | 
				
			||||||
 | 
					        containers={@containers}
 | 
				
			||||||
  <div class="max-w-full flex flex-row flex-wrap justify-center items-center">
 | 
					        current_user={@current_user}
 | 
				
			||||||
    <%= for container <- @containers do %>
 | 
					      >
 | 
				
			||||||
      <.container_card container={container}>
 | 
					        <:tag_actions :let={container}>
 | 
				
			||||||
        <:tag_actions>
 | 
					 | 
				
			||||||
          <div class="mx-4 my-2">
 | 
					          <div class="mx-4 my-2">
 | 
				
			||||||
            <%= live_patch to: Routes.container_index_path(Endpoint, :edit_tags, container),
 | 
					            <.link
 | 
				
			||||||
              class: "text-primary-600 link" do %>
 | 
					              patch={Routes.container_index_path(Endpoint, :edit_tags, container)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
					              <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
				
			||||||
            <% end %>
 | 
					            </.link>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </:tag_actions>
 | 
					        </:tag_actions>
 | 
				
			||||||
        <%= live_patch to: Routes.container_index_path(Endpoint, :edit, container),
 | 
					        <:actions :let={container}>
 | 
				
			||||||
                   class: "text-primary-600 link",
 | 
					          <.link
 | 
				
			||||||
                   data: [qa: "edit-#{container.id}"] do %>
 | 
					            patch={Routes.container_index_path(Endpoint, :edit, container)}
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
        <% end %>
 | 
					            data-qa={"edit-#{container.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= link to: "#",
 | 
					          <.link
 | 
				
			||||||
             class: "text-primary-600 link",
 | 
					            patch={Routes.container_index_path(Endpoint, :clone, container)}
 | 
				
			||||||
             phx_click: "delete",
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
             phx_value_id: container.id,
 | 
					            data-qa={"clone-#{container.id}"}
 | 
				
			||||||
             data: [
 | 
					          >
 | 
				
			||||||
               confirm:
 | 
					            <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
                 dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name),
 | 
					          </.link>
 | 
				
			||||||
               qa: "delete-#{container.id}"
 | 
					
 | 
				
			||||||
             ] do %>
 | 
					          <.link
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					            href="#"
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            phx-click="delete"
 | 
				
			||||||
 | 
					            phx-value-id={container.id}
 | 
				
			||||||
 | 
					            data-confirm={
 | 
				
			||||||
 | 
					              dgettext("prompts", "Are you sure you want to delete %{name}?", name: container.name)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            data-qa={"delete-#{container.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </:actions>
 | 
				
			||||||
 | 
					      </.live_component>
 | 
				
			||||||
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
 | 
					        <%= for container <- @containers do %>
 | 
				
			||||||
 | 
					          <.container_card container={container}>
 | 
				
			||||||
 | 
					            <:tag_actions>
 | 
				
			||||||
 | 
					              <div class="mx-4 my-2">
 | 
				
			||||||
 | 
					                <.link
 | 
				
			||||||
 | 
					                  patch={Routes.container_index_path(Endpoint, :edit_tags, container)}
 | 
				
			||||||
 | 
					                  class="text-primary-600 link"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
				
			||||||
 | 
					                </.link>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </:tag_actions>
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.container_index_path(Endpoint, :edit, container)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"edit-#{container.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.container_index_path(Endpoint, :clone, container)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"clone-#{container.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              href="#"
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              phx-click="delete"
 | 
				
			||||||
 | 
					              phx-value-id={container.id}
 | 
				
			||||||
 | 
					              data-confirm={
 | 
				
			||||||
 | 
					                dgettext("prompts", "Are you sure you want to delete %{name}?",
 | 
				
			||||||
 | 
					                  name: container.name
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              data-qa={"delete-#{container.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </.container_card>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
      </.container_card>
 | 
					      </div>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action in [:new, :edit] do %>
 | 
					<%= if @live_action in [:new, :edit, :clone] do %>
 | 
				
			||||||
  <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
 | 
					  <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
 | 
				
			||||||
    <.live_component
 | 
					    <.live_component
 | 
				
			||||||
      module={CanneryWeb.ContainerLive.FormComponent}
 | 
					      module={CanneryWeb.ContainerLive.FormComponent}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,23 +5,23 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  import CanneryWeb.Components.{AmmoGroupCard, TagCard}
 | 
					  import CanneryWeb.Components.{AmmoGroupCard, TagCard}
 | 
				
			||||||
  alias Cannery.{Accounts.User, Containers, Containers.Container, Repo, Tags}
 | 
					  alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container, Repo, Tags}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(_params, _session, socket),
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session)}
 | 
					    do: {:ok, socket |> assign(show_used: false, view_table: true)}
 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(
 | 
					  def handle_params(%{"id" => id}, _session, %{assigns: %{current_user: current_user}} = socket) do
 | 
				
			||||||
        %{"id" => id},
 | 
					    socket =
 | 
				
			||||||
        _session,
 | 
					      socket
 | 
				
			||||||
        %{assigns: %{current_user: current_user}} = socket
 | 
					      |> assign(view_table: true)
 | 
				
			||||||
      ) do
 | 
					      |> render_container(id, current_user)
 | 
				
			||||||
    {:noreply, socket |> render_container(id, current_user)}
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -41,7 +41,7 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
              container_name: container.name
 | 
					              container_name: container.name
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> render_container(container.id, current_user)
 | 
					          socket |> put_flash(:info, prompt) |> render_container()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, error_string} ->
 | 
					        {:error, error_string} ->
 | 
				
			||||||
          socket |> put_flash(:error, error_string)
 | 
					          socket |> put_flash(:error, error_string)
 | 
				
			||||||
@@ -64,7 +64,7 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          socket
 | 
					          socket
 | 
				
			||||||
          |> put_flash(:info, prompt)
 | 
					          |> put_flash(:info, prompt)
 | 
				
			||||||
          |> push_redirect(to: Routes.container_index_path(socket, :index))
 | 
					          |> push_navigate(to: Routes.container_index_path(socket, :index))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %{action: :delete, errors: [ammo_groups: _error], valid?: false} = changeset} ->
 | 
					        {:error, %{action: :delete, errors: [ammo_groups: _error], valid?: false} = changeset} ->
 | 
				
			||||||
          ammo_groups_error = changeset |> changeset_errors(:ammo_groups) |> Enum.join(", ")
 | 
					          ammo_groups_error = changeset |> changeset_errors(:ammo_groups) |> Enum.join(", ")
 | 
				
			||||||
@@ -84,20 +84,43 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec render_container(Socket.t(), Container.id(), User.t()) :: Socket.t()
 | 
					  @spec render_container(Socket.t(), Container.id(), User.t()) :: Socket.t()
 | 
				
			||||||
  defp render_container(%{assigns: %{live_action: live_action}} = socket, id, current_user) do
 | 
					  defp render_container(
 | 
				
			||||||
 | 
					         %{assigns: %{live_action: live_action, show_used: show_used}} = socket,
 | 
				
			||||||
 | 
					         id,
 | 
				
			||||||
 | 
					         current_user
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
    %{name: container_name} =
 | 
					    %{name: container_name} =
 | 
				
			||||||
      container =
 | 
					      container =
 | 
				
			||||||
      Containers.get_container!(id, current_user)
 | 
					      Containers.get_container!(id, current_user)
 | 
				
			||||||
      |> Repo.preload([:ammo_groups, :tags], force: true)
 | 
					      |> Repo.preload([:tags], force: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ammo_groups = Ammo.list_ammo_groups_for_container(container, current_user, show_used)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    page_title =
 | 
					    page_title =
 | 
				
			||||||
      case live_action do
 | 
					      case live_action do
 | 
				
			||||||
        :show -> gettext("Show %{name}", name: container_name)
 | 
					        action when action in [:show, :table] -> container_name
 | 
				
			||||||
        :edit -> gettext("Edit %{name}", name: container_name)
 | 
					        :edit -> gettext("Edit %{name}", name: container_name)
 | 
				
			||||||
        :edit_tags -> gettext("Edit %{name} tags", name: container_name)
 | 
					        :edit_tags -> gettext("Edit %{name} tags", name: container_name)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket |> assign(container: container, page_title: page_title)
 | 
					    socket |> assign(container: container, ammo_groups: ammo_groups, page_title: page_title)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec render_container(Socket.t()) :: Socket.t()
 | 
				
			||||||
 | 
					  defp render_container(
 | 
				
			||||||
 | 
					         %{assigns: %{container: %{id: container_id}, current_user: current_user}} = socket
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    socket |> render_container(container_id, current_user)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="mx-auto space-y-4 max-w-3xl flex flex-col justify-center items-center">
 | 
					<div class="space-y-4 flex flex-col justify-center items-center">
 | 
				
			||||||
  <h1 class="title text-2xl title-primary-500">
 | 
					  <h1 class="title text-2xl title-primary-500">
 | 
				
			||||||
    <%= @container.name %>
 | 
					    <%= @container.name %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
@@ -22,23 +22,46 @@
 | 
				
			|||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex space-x-4 justify-center items-center text-primary-600">
 | 
					  <%= unless @ammo_groups |> Enum.empty?() do %>
 | 
				
			||||||
    <%= live_patch to: Routes.container_show_path(Endpoint, :edit, @container),
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
               class: "text-primary-600 link",
 | 
					      <%= if @show_used do %>
 | 
				
			||||||
               data: [qa: "edit"] do %>
 | 
					        <%= gettext("Total packs:") %>
 | 
				
			||||||
      <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					      <% else %>
 | 
				
			||||||
    <% end %>
 | 
					        <%= gettext("Packs:") %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					      <%= Enum.count(@ammo_groups) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= link to: "#",
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
         class: "text-primary-600 link",
 | 
					      <%= if @show_used do %>
 | 
				
			||||||
         phx_click: "delete_container",
 | 
					        <%= gettext("Total rounds:") %>
 | 
				
			||||||
         data: [
 | 
					      <% else %>
 | 
				
			||||||
           confirm:
 | 
					        <%= gettext("Rounds:") %>
 | 
				
			||||||
             dgettext("prompts", "Are you sure you want to delete %{name}?", name: @container.name),
 | 
					      <% end %>
 | 
				
			||||||
           qa: "delete"
 | 
					      <%= @container |> Containers.get_container_rounds!() %>
 | 
				
			||||||
         ] do %>
 | 
					    </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"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.link
 | 
				
			||||||
 | 
					      href="#"
 | 
				
			||||||
 | 
					      class="text-primary-600 link"
 | 
				
			||||||
 | 
					      phx-click="delete_container"
 | 
				
			||||||
 | 
					      data-confirm={
 | 
				
			||||||
 | 
					        dgettext("prompts", "Are you sure you want to delete %{name}?", name: @container.name)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      data-qa="delete"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					      <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
    <% end %>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="mb-4 hr" />
 | 
					  <hr class="mb-4 hr" />
 | 
				
			||||||
@@ -50,10 +73,12 @@
 | 
				
			|||||||
        <%= display_emoji("😔") %>
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Why not add one?"),
 | 
					      <.link
 | 
				
			||||||
        to: Routes.container_show_path(Endpoint, :edit_tags, @container),
 | 
					        patch={Routes.container_show_path(Endpoint, :edit_tags, @container)}
 | 
				
			||||||
        class: "btn btn-primary"
 | 
					        class="btn btn-primary"
 | 
				
			||||||
      ) %>
 | 
					      >
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Why not add one?") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <div class="flex flex-wrap justify-center items-center">
 | 
					    <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
@@ -62,27 +87,58 @@
 | 
				
			|||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="mx-4 my-2">
 | 
					      <div class="mx-4 my-2">
 | 
				
			||||||
        <%= live_patch to: Routes.container_show_path(Endpoint, :edit_tags, @container),
 | 
					        <.link
 | 
				
			||||||
          class: "text-primary-600 link" do %>
 | 
					          patch={Routes.container_show_path(Endpoint, :edit_tags, @container)}
 | 
				
			||||||
 | 
					          class="text-primary-600 link"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
					          <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="mb-4 hr" />
 | 
					  <hr class="mb-4 hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div>
 | 
					  <div class="flex justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if @container.ammo_groups |> Enum.empty?() do %>
 | 
					    <.toggle_button action="toggle_show_used" value={@show_used}>
 | 
				
			||||||
      <h2 class="mx-8 my-4 title text-lg text-primary-600">
 | 
					      <span class="title text-lg text-primary-600">
 | 
				
			||||||
        <%= gettext("No ammo groups in this container") %>
 | 
					        <%= gettext("Show used") %>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    </.toggle_button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.toggle_button action="toggle_table" value={@view_table}>
 | 
				
			||||||
 | 
					      <span class="title text-lg text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("View as table") %>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    </.toggle_button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="w-full p-4">
 | 
				
			||||||
 | 
					    <%= if @ammo_groups |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					      <h2 class="mx-4 title text-lg text-primary-600 text-center">
 | 
				
			||||||
 | 
					        <%= gettext("No ammo in this container") %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
    <% else %>
 | 
					    <% else %>
 | 
				
			||||||
      <div class="flex flex-wrap justify-center items-center">
 | 
					      <%= if @view_table do %>
 | 
				
			||||||
        <%= for ammo_group <- @container.ammo_groups do %>
 | 
					        <.live_component
 | 
				
			||||||
          <.ammo_group_card ammo_group={ammo_group} />
 | 
					          module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
        <% end %>
 | 
					          id="ammo-type-show-table"
 | 
				
			||||||
      </div>
 | 
					          ammo_groups={@ammo_groups}
 | 
				
			||||||
 | 
					          current_user={@current_user}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <:ammo_type :let={%{name: ammo_type_name} = ammo_type}>
 | 
				
			||||||
 | 
					            <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link">
 | 
				
			||||||
 | 
					              <%= ammo_type_name %>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </:ammo_type>
 | 
				
			||||||
 | 
					        </.live_component>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <div class="flex flex-wrap justify-center items-stretch">
 | 
				
			||||||
 | 
					          <%= for ammo_group <- @ammo_groups do %>
 | 
				
			||||||
 | 
					            <.ammo_group_card ammo_group={ammo_group} />
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,42 +5,28 @@ defmodule CanneryWeb.HomeLive do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  alias Cannery.Accounts
 | 
					  alias Cannery.Accounts
 | 
				
			||||||
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @version Mix.Project.config()[:version]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(_params, _session, socket) do
 | 
				
			||||||
    admins = Accounts.list_users_by_role(:admin)
 | 
					    admins = Accounts.list_users_by_role(:admin)
 | 
				
			||||||
 | 
					    socket = socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)
 | 
				
			||||||
    socket =
 | 
					 | 
				
			||||||
      socket
 | 
					 | 
				
			||||||
      |> assign_defaults(session)
 | 
					 | 
				
			||||||
      |> assign(page_title: "Home", query: "", results: %{}, admins: admins)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {:ok, socket}
 | 
					    {:ok, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					 | 
				
			||||||
  def handle_event("suggest", %{"q" => query}, socket) do
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(results: search(query), query: query)}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @impl true
 | 
					 | 
				
			||||||
  def handle_event("search", %{"q" => query}, socket) do
 | 
					 | 
				
			||||||
    case search(query) do
 | 
					 | 
				
			||||||
      %{^query => vsn} ->
 | 
					 | 
				
			||||||
        {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      _no_query ->
 | 
					 | 
				
			||||||
        {:noreply,
 | 
					 | 
				
			||||||
         socket
 | 
					 | 
				
			||||||
         |> put_flash(:error, "No dependencies found matching \"#{query}\"")
 | 
					 | 
				
			||||||
         |> assign(results: %{}, query: query)}
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def render(assigns) do
 | 
					  def render(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl">
 | 
					    <div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl">
 | 
				
			||||||
 | 
					      <img
 | 
				
			||||||
 | 
					        src={Routes.static_path(Endpoint, "/images/cannery.svg")}
 | 
				
			||||||
 | 
					        alt={gettext("Cannery logo")}
 | 
				
			||||||
 | 
					        class="inline-block w-32 hover:-mt-2 hover:mb-2 transition-all duration-500 ease-in-out"
 | 
				
			||||||
 | 
					        title={gettext("isn't he cute >:3")}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <h1 class="title text-primary-600 text-2xl">
 | 
					      <h1 class="title text-primary-600 text-2xl">
 | 
				
			||||||
        <%= gettext("Welcome to %{name}", name: "Cannery") %>
 | 
					        <%= gettext("Welcome to %{name}", name: "Cannery") %>
 | 
				
			||||||
      </h1>
 | 
					      </h1>
 | 
				
			||||||
@@ -98,10 +84,12 @@ defmodule CanneryWeb.HomeLive do
 | 
				
			|||||||
          </b>
 | 
					          </b>
 | 
				
			||||||
          <p>
 | 
					          <p>
 | 
				
			||||||
            <%= if @admins |> Enum.empty?() do %>
 | 
					            <%= if @admins |> Enum.empty?() do %>
 | 
				
			||||||
              <%= link(dgettext("prompts", "Register to setup %{name}", name: "Cannery"),
 | 
					              <.link
 | 
				
			||||||
                class: "hover:underline",
 | 
					                href={Routes.user_registration_path(CanneryWeb.Endpoint, :new)}
 | 
				
			||||||
                to: Routes.user_registration_path(CanneryWeb.Endpoint, :new)
 | 
					                class="hover:underline"
 | 
				
			||||||
              ) %>
 | 
					              >
 | 
				
			||||||
 | 
					                <%= dgettext("prompts", "Register to setup %{name}", name: "Cannery") %>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
            <% else %>
 | 
					            <% else %>
 | 
				
			||||||
              <div class="flex flex-wrap justify-center space-x-2">
 | 
					              <div class="flex flex-wrap justify-center space-x-2">
 | 
				
			||||||
                <%= for admin <- @admins do %>
 | 
					                <%= for admin <- @admins do %>
 | 
				
			||||||
@@ -127,28 +115,62 @@ defmodule CanneryWeb.HomeLive do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <li class="flex flex-row justify-center items-center space-x-2">
 | 
					        <li class="flex flex-row justify-center items-center space-x-2">
 | 
				
			||||||
          <b>Version:</b>
 | 
					          <b>Version:</b>
 | 
				
			||||||
          <%= link class: "flex flex-row justify-center items-center space-x-2 hover:underline",
 | 
					          <.link
 | 
				
			||||||
                to: "https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/CHANGELOG.md",
 | 
					            href="https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/CHANGELOG.md"
 | 
				
			||||||
                target: "_blank",
 | 
					            class="flex flex-row justify-center items-center space-x-2 hover:underline"
 | 
				
			||||||
                rel: "noopener noreferrer" do %>
 | 
					            target="_blank"
 | 
				
			||||||
            <p>0.5.1</p>
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p>
 | 
				
			||||||
 | 
					              <%= @version %>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
            <i class="fas fa-md fa-info-circle"></i>
 | 
					            <i class="fas fa-md fa-info-circle"></i>
 | 
				
			||||||
          <% end %>
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ul class="flex flex-col space-y-2 text-center justify-center">
 | 
				
			||||||
 | 
					        <h2 class="title text-primary-600 text-lg">
 | 
				
			||||||
 | 
					          <%= gettext("Get involved!") %>
 | 
				
			||||||
 | 
					        </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <li class="flex flex-col justify-center space-x-2">
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            class="flex flex-row justify-center items-center space-x-2 hover:underline"
 | 
				
			||||||
 | 
					            href="https://gitea.bubbletea.dev/shibao/cannery"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p><%= gettext("View the source code") %></p>
 | 
				
			||||||
 | 
					            <i class="fas fa-md fa-code"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        <li class="flex flex-col justify-center space-x-2">
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            class="flex flex-row justify-center items-center space-x-2 hover:underline"
 | 
				
			||||||
 | 
					            href="https://weblate.bubbletea.dev/engage/cannery"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p><%= gettext("Help translate") %></p>
 | 
				
			||||||
 | 
					            <i class="fas fa-md fa-language"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        <li class="flex flex-col justify-center space-x-2">
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            class="flex flex-row justify-center items-center space-x-2 hover:underline"
 | 
				
			||||||
 | 
					            href="https://gitea.bubbletea.dev/shibao/cannery/issues/new"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p><%= gettext("Report bugs or request features") %></p>
 | 
				
			||||||
 | 
					            <i class="fas fa-md fa-spider"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp search(query) do
 | 
					 | 
				
			||||||
    if not CanneryWeb.Endpoint.config(:code_reloader) do
 | 
					 | 
				
			||||||
      raise "action disabled when not in development"
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for {app, desc, vsn} <- Application.started_applications(),
 | 
					 | 
				
			||||||
        app = to_string(app),
 | 
					 | 
				
			||||||
        String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
 | 
					 | 
				
			||||||
        into: %{},
 | 
					 | 
				
			||||||
        do: {app, vsn}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								lib/cannery_web/live/init_assigns.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/cannery_web/live/init_assigns.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.InitAssigns do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Ensures common `assigns` are applied to all LiveViews attaching this hook.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  import Phoenix.Component
 | 
				
			||||||
 | 
					  alias Cannery.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def on_mount(:default, _params, %{"locale" => locale, "user_token" => user_token}, socket) do
 | 
				
			||||||
 | 
					    Gettext.put_locale(locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign_new(:current_user, fn -> Accounts.get_user_by_session_token(user_token) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:cont, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def on_mount(:default, _params, _session, socket), do: {:cont, socket}
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -13,23 +13,44 @@ defmodule CanneryWeb.InviteLive.FormComponent do
 | 
				
			|||||||
          %{:invite => Invite.t(), :current_user => User.t(), optional(any) => any},
 | 
					          %{:invite => Invite.t(), :current_user => User.t(), optional(any) => any},
 | 
				
			||||||
          Socket.t()
 | 
					          Socket.t()
 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{invite: invite} = assigns, socket) do
 | 
					  def update(%{invite: _invite} = assigns, socket) do
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:changeset, Invites.change_invite(invite))}
 | 
					    {:ok, socket |> assign(assigns) |> assign_changeset(%{})}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event("validate", %{"invite" => invite_params}, socket) do
 | 
				
			||||||
        "validate",
 | 
					    {:noreply, socket |> assign_changeset(invite_params)}
 | 
				
			||||||
        %{"invite" => invite_params},
 | 
					 | 
				
			||||||
        %{assigns: %{invite: invite}} = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, invite |> Invites.change_invite(invite_params))}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
 | 
					  def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
 | 
				
			||||||
    save_invite(socket, action, invite_params)
 | 
					    save_invite(socket, action, invite_params)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp assign_changeset(
 | 
				
			||||||
 | 
					         %{assigns: %{action: action, current_user: user, invite: invite}} = socket,
 | 
				
			||||||
 | 
					         invite_params
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    changeset_action =
 | 
				
			||||||
 | 
					      case action do
 | 
				
			||||||
 | 
					        :new -> :insert
 | 
				
			||||||
 | 
					        :edit -> :update
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case action do
 | 
				
			||||||
 | 
					        :new -> Invite.create_changeset(user, "example_token", invite_params)
 | 
				
			||||||
 | 
					        :edit -> invite |> Invite.update_changeset(invite_params)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case changeset |> Changeset.apply_action(changeset_action) do
 | 
				
			||||||
 | 
					        {:ok, _data} -> changeset
 | 
				
			||||||
 | 
					        {:error, changeset} -> changeset
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(:changeset, changeset)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp save_invite(
 | 
					  defp save_invite(
 | 
				
			||||||
         %{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket,
 | 
					         %{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket,
 | 
				
			||||||
         :edit,
 | 
					         :edit,
 | 
				
			||||||
@@ -39,7 +60,7 @@ defmodule CanneryWeb.InviteLive.FormComponent do
 | 
				
			|||||||
      case invite |> Invites.update_invite(invite_params, current_user) do
 | 
					      case invite |> Invites.update_invite(invite_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: invite_name}} ->
 | 
					        {:ok, %{name: invite_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} updated successfully", name: invite_name)
 | 
					          prompt = dgettext("prompts", "%{name} updated successfully", name: invite_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
@@ -57,7 +78,7 @@ defmodule CanneryWeb.InviteLive.FormComponent do
 | 
				
			|||||||
      case current_user |> Invites.create_invite(invite_params) do
 | 
					      case current_user |> Invites.create_invite(invite_params) do
 | 
				
			||||||
        {:ok, %{name: invite_name}} ->
 | 
					        {:ok, %{name: invite_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} created successfully", name: invite_name)
 | 
					          prompt = dgettext("prompts", "%{name} created successfully", name: invite_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
    <%= @title %>
 | 
					    <%= @title %>
 | 
				
			||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="invite-form"
 | 
					    id="invite-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"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
@@ -24,6 +24,9 @@
 | 
				
			|||||||
    <%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %>
 | 
					    <%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %>
 | 
					    <%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :uses_left, "col-span-3") %>
 | 
					    <%= error_tag(f, :uses_left, "col-span-3") %>
 | 
				
			||||||
 | 
					    <span class="col-span-3 text-primary-400 italic text-center">
 | 
				
			||||||
 | 
					      <%= gettext("Leave \"Uses left\" blank to make invite unlimited") %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= submit(dgettext("actions", "Save"),
 | 
					    <%= submit(dgettext("actions", "Save"),
 | 
				
			||||||
      class: "mx-auto btn btn-primary col-span-3",
 | 
					      class: "mx-auto btn btn-primary col-span-3",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,16 +10,14 @@ defmodule CanneryWeb.InviteLive.Index do
 | 
				
			|||||||
  alias Phoenix.LiveView.JS
 | 
					  alias Phoenix.LiveView.JS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
 | 
				
			||||||
    %{assigns: %{current_user: current_user}} = socket = socket |> assign_defaults(session)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      if current_user |> Map.get(:role) == :admin do
 | 
					      if current_user |> Map.get(:role) == :admin do
 | 
				
			||||||
        socket |> display_invites()
 | 
					        socket |> display_invites()
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        prompt = dgettext("errors", "You are not authorized to view this page")
 | 
					        prompt = dgettext("errors", "You are not authorized to view this page")
 | 
				
			||||||
        return_to = Routes.live_path(Endpoint, HomeLive)
 | 
					        return_to = Routes.live_path(Endpoint, HomeLive)
 | 
				
			||||||
        socket |> put_flash(:error, prompt) |> push_redirect(to: return_to)
 | 
					        socket |> put_flash(:error, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:ok, socket}
 | 
					    {:ok, socket}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,18 +9,16 @@
 | 
				
			|||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Invite someone new!"),
 | 
					    <.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.invite_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "Invite someone new!") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Create Invite"),
 | 
					    <.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.invite_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "Create Invite") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="w-full flex flex-row flex-wrap justify-center items-center">
 | 
					  <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
    <%= for invite <- @invites do %>
 | 
					    <%= for invite <- @invites do %>
 | 
				
			||||||
      <.invite_card invite={invite}>
 | 
					      <.invite_card invite={invite}>
 | 
				
			||||||
        <:code_actions>
 | 
					        <:code_actions>
 | 
				
			||||||
@@ -34,33 +32,36 @@
 | 
				
			|||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
          </form>
 | 
					          </form>
 | 
				
			||||||
        </:code_actions>
 | 
					        </:code_actions>
 | 
				
			||||||
        <%= live_patch to: Routes.invite_index_path(Endpoint, :edit, invite),
 | 
					        <.link
 | 
				
			||||||
                   class: "text-primary-600 link",
 | 
					          patch={Routes.invite_index_path(Endpoint, :edit, invite)}
 | 
				
			||||||
                   data: [qa: "edit-#{invite.id}"] do %>
 | 
					          class="text-primary-600 link"
 | 
				
			||||||
 | 
					          data-qa={"edit-#{invite.id}"}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					          <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= link to: "#",
 | 
					        <.link
 | 
				
			||||||
             class: "text-primary-600 link",
 | 
					          href="#"
 | 
				
			||||||
             phx_click: "delete_invite",
 | 
					          class="text-primary-600 link"
 | 
				
			||||||
             phx_value_id: invite.id,
 | 
					          phx-click="delete_invite"
 | 
				
			||||||
             data: [
 | 
					          phx-value-id={invite.id}
 | 
				
			||||||
               confirm:
 | 
					          data-confirm={
 | 
				
			||||||
                 dgettext("prompts", "Are you sure you want to delete the invite for %{name}?",
 | 
					            dgettext("prompts", "Are you sure you want to delete the invite for %{name}?",
 | 
				
			||||||
                   name: invite.name
 | 
					              name: invite.name
 | 
				
			||||||
                 ),
 | 
					            )
 | 
				
			||||||
               qa: "delete-#{invite.id}"
 | 
					          }
 | 
				
			||||||
             ] do %>
 | 
					          data-qa={"delete-#{invite.id}"}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					          <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= if invite.disabled_at |> is_nil() do %>
 | 
					        <%= if invite.disabled_at |> is_nil() do %>
 | 
				
			||||||
          <a href="#" class="btn btn-primary" phx-click="disable_invite" phx-value-id={invite.id}>
 | 
					          <a href="#" class="btn btn-primary" phx-click="disable_invite" phx-value-id={invite.id}>
 | 
				
			||||||
            <%= gettext("Disable") %>
 | 
					            <%= dgettext("actions", "Disable") %>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        <% else %>
 | 
					        <% else %>
 | 
				
			||||||
          <a href="#" class="btn btn-primary" phx-click="enable_invite" phx-value-id={invite.id}>
 | 
					          <a href="#" class="btn btn-primary" phx-click="enable_invite" phx-value-id={invite.id}>
 | 
				
			||||||
            <%= gettext("Enable") %>
 | 
					            <%= dgettext("actions", "Enable") %>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -76,7 +77,7 @@
 | 
				
			|||||||
              )
 | 
					              )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <%= gettext("Set Unlimited") %>
 | 
					            <%= dgettext("actions", "Set Unlimited") %>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
      </.invite_card>
 | 
					      </.invite_card>
 | 
				
			||||||
@@ -90,23 +91,24 @@
 | 
				
			|||||||
      <%= gettext("Admins") %>
 | 
					      <%= gettext("Admins") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full flex flex-row flex-wrap justify-center items-center">
 | 
					    <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
      <%= for admin <- @admins do %>
 | 
					      <%= for admin <- @admins do %>
 | 
				
			||||||
        <.user_card user={admin}>
 | 
					        <.user_card user={admin}>
 | 
				
			||||||
          <%= link to: "#",
 | 
					          <.link
 | 
				
			||||||
               class: "text-primary-600 link",
 | 
					            href="#"
 | 
				
			||||||
               phx_click: "delete_user",
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
               phx_value_id: admin.id,
 | 
					            phx-click="delete_user"
 | 
				
			||||||
               data: [
 | 
					            phx-value-id={admin.id}
 | 
				
			||||||
                 confirm:
 | 
					            data-confirm={
 | 
				
			||||||
                   dgettext(
 | 
					              dgettext(
 | 
				
			||||||
                     "prompts",
 | 
					                "prompts",
 | 
				
			||||||
                     "Are you sure you want to delete %{email}? This action is permanent!",
 | 
					                "Are you sure you want to delete %{email}? This action is permanent!",
 | 
				
			||||||
                     email: admin.email
 | 
					                email: admin.email
 | 
				
			||||||
                   )
 | 
					              )
 | 
				
			||||||
               ] do %>
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
          <% end %>
 | 
					          </.link>
 | 
				
			||||||
        </.user_card>
 | 
					        </.user_card>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -119,23 +121,24 @@
 | 
				
			|||||||
      <%= gettext("Users") %>
 | 
					      <%= gettext("Users") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full flex flex-row flex-wrap justify-center items-center">
 | 
					    <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
      <%= for user <- @users do %>
 | 
					      <%= for user <- @users do %>
 | 
				
			||||||
        <.user_card user={user}>
 | 
					        <.user_card user={user}>
 | 
				
			||||||
          <%= link to: "#",
 | 
					          <.link
 | 
				
			||||||
               class: "text-primary-600 link",
 | 
					            href="#"
 | 
				
			||||||
               phx_click: "delete_user",
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
               phx_value_id: user.id,
 | 
					            phx-click="delete_user"
 | 
				
			||||||
               data: [
 | 
					            phx-value-id={user.id}
 | 
				
			||||||
                 confirm:
 | 
					            data-confirm={
 | 
				
			||||||
                   dgettext(
 | 
					              dgettext(
 | 
				
			||||||
                     "prompts",
 | 
					                "prompts",
 | 
				
			||||||
                     "Are you sure you want to delete %{email}? This action is permanent!",
 | 
					                "Are you sure you want to delete %{email}? This action is permanent!",
 | 
				
			||||||
                     email: user.email
 | 
					                email: user.email
 | 
				
			||||||
                   )
 | 
					              )
 | 
				
			||||||
               ] do %>
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
          <% end %>
 | 
					          </.link>
 | 
				
			||||||
        </.user_card>
 | 
					        </.user_card>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,20 +3,9 @@ defmodule CanneryWeb.LiveHelpers do
 | 
				
			|||||||
  Contains common helper functions for liveviews
 | 
					  Contains common helper functions for liveviews
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import Phoenix.LiveView
 | 
					  import Phoenix.Component
 | 
				
			||||||
  import Phoenix.LiveView.Helpers
 | 
					 | 
				
			||||||
  alias Cannery.Accounts
 | 
					 | 
				
			||||||
  alias Phoenix.LiveView.JS
 | 
					  alias Phoenix.LiveView.JS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def assign_defaults(socket, %{"user_token" => user_token} = _session) do
 | 
					 | 
				
			||||||
    socket
 | 
					 | 
				
			||||||
    |> assign_new(:current_user, fn -> Accounts.get_user_by_session_token(user_token) end)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def assign_defaults(socket, _session) do
 | 
					 | 
				
			||||||
    socket
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Renders a live component inside a modal.
 | 
					  Renders a live component inside a modal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,17 +27,17 @@ defmodule CanneryWeb.LiveHelpers do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  def modal(assigns) do
 | 
					  def modal(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <%= live_patch to: @return_to,
 | 
					    <.link
 | 
				
			||||||
      id: "modal-bg",
 | 
					      patch={@return_to}
 | 
				
			||||||
      class:
 | 
					      id="modal-bg"
 | 
				
			||||||
        "fade-in fixed z-10 left-0 top-0
 | 
					      class="fade-in fixed z-10 left-0 top-0
 | 
				
			||||||
         w-full h-full overflow-hidden
 | 
					         w-full h-full overflow-hidden
 | 
				
			||||||
         p-8 flex flex-col justify-center items-center cursor-auto",
 | 
					         p-8 flex flex-col justify-center items-center cursor-auto"
 | 
				
			||||||
      style: "background-color: rgba(0,0,0,0.4);",
 | 
					      style="background-color: rgba(0,0,0,0.4);"
 | 
				
			||||||
      phx_remove: hide_modal()
 | 
					      phx_remove={hide_modal()}
 | 
				
			||||||
    do %>
 | 
					    >
 | 
				
			||||||
      <span class="hidden"></span>
 | 
					      <span class="hidden"></span>
 | 
				
			||||||
    <% end %>
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      id="modal"
 | 
					      id="modal"
 | 
				
			||||||
@@ -64,15 +53,16 @@ defmodule CanneryWeb.LiveHelpers do
 | 
				
			|||||||
        flex flex-col justify-start items-center
 | 
					        flex flex-col justify-start items-center
 | 
				
			||||||
        bg-white border-2 rounded-lg"
 | 
					        bg-white border-2 rounded-lg"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <%= live_patch to: @return_to,
 | 
					        <.link
 | 
				
			||||||
                   id: "close",
 | 
					          patch={@return_to}
 | 
				
			||||||
                   class:
 | 
					          id="close"
 | 
				
			||||||
                     "absolute top-8 right-10
 | 
					          class="absolute top-8 right-10
 | 
				
			||||||
                      text-gray-500 hover:text-gray-800
 | 
					                      text-gray-500 hover:text-gray-800
 | 
				
			||||||
                      transition-all duration-500 ease-in-out",
 | 
					                      transition-all duration-500 ease-in-out"
 | 
				
			||||||
                   phx_remove: hide_modal() do %>
 | 
					          phx_remove={hide_modal()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-times"></i>
 | 
					          <i class="fa-fw fa-lg fas fa-times"></i>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
 | 
					        <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) %>
 | 
					          <%= render_slot(@inner_block) %>
 | 
				
			||||||
@@ -82,10 +72,57 @@ defmodule CanneryWeb.LiveHelpers do
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def hide_modal(js \\ %JS{}) do
 | 
					  defp hide_modal(js \\ %JS{}) do
 | 
				
			||||||
    js
 | 
					    js
 | 
				
			||||||
    |> JS.hide(to: "#modal", transition: "fade-out")
 | 
					    |> JS.hide(to: "#modal", transition: "fade-out")
 | 
				
			||||||
    |> JS.hide(to: "#modal-bg", transition: "fade-out")
 | 
					    |> JS.hide(to: "#modal-bg", transition: "fade-out")
 | 
				
			||||||
    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
 | 
					    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
 | 
				
			||||||
  end
 | 
					  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
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
defmodule CanneryWeb.RangeLive.FormComponent do
 | 
					defmodule CanneryWeb.RangeLive.FormComponent do
 | 
				
			||||||
  @moduledoc """
 | 
					  @moduledoc """
 | 
				
			||||||
  Livecomponent that can update or create a ShotGroup
 | 
					  Livecomponent that can update a ShotGroup
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_component
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
@@ -24,7 +24,7 @@ defmodule CanneryWeb.RangeLive.FormComponent do
 | 
				
			|||||||
        } = assigns,
 | 
					        } = assigns,
 | 
				
			||||||
        socket
 | 
					        socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    changeset = shot_group |> ActivityLog.change_shot_group()
 | 
					    changeset = shot_group |> ShotGroup.update_changeset(current_user, %{})
 | 
				
			||||||
    ammo_group = Ammo.get_ammo_group!(ammo_group_id, current_user)
 | 
					    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, changeset: changeset)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -33,11 +33,11 @@ defmodule CanneryWeb.RangeLive.FormComponent do
 | 
				
			|||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "validate",
 | 
					        "validate",
 | 
				
			||||||
        %{"shot_group" => shot_group_params},
 | 
					        %{"shot_group" => shot_group_params},
 | 
				
			||||||
        %{assigns: %{shot_group: shot_group}} = socket
 | 
					        %{assigns: %{current_user: current_user, shot_group: shot_group}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    changeset =
 | 
					    changeset =
 | 
				
			||||||
      shot_group
 | 
					      shot_group
 | 
				
			||||||
      |> ActivityLog.change_shot_group(shot_group_params)
 | 
					      |> ShotGroup.update_changeset(current_user, shot_group_params)
 | 
				
			||||||
      |> Map.put(:action, :validate)
 | 
					      |> Map.put(:action, :validate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, assign(socket, :changeset, changeset)}
 | 
					    {:noreply, assign(socket, :changeset, changeset)}
 | 
				
			||||||
@@ -53,7 +53,7 @@ defmodule CanneryWeb.RangeLive.FormComponent do
 | 
				
			|||||||
      case ActivityLog.update_shot_group(shot_group, shot_group_params, current_user) do
 | 
					      case ActivityLog.update_shot_group(shot_group, shot_group_params, current_user) do
 | 
				
			||||||
        {:ok, _shot_group} ->
 | 
					        {:ok, _shot_group} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "Shot records updated successfully")
 | 
					          prompt = dgettext("prompts", "Shot records updated successfully")
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Ecto.Changeset{} = changeset} ->
 | 
					        {:error, %Ecto.Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="shot-group-form"
 | 
					    id="shot-group-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"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
@@ -33,7 +33,7 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :notes, "col-span-3") %>
 | 
					    <%= error_tag(f, :notes, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :date, gettext("Date (UTC)"), class: "title text-lg text-primary-600") %>
 | 
					    <%= label(f, :date, gettext("Date"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= date_input(f, :date, class: "input input-primary col-span-2") %>
 | 
					    <%= date_input(f, :date, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :notes, "col-span-3") %>
 | 
					    <%= error_tag(f, :notes, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,8 +10,12 @@ defmodule CanneryWeb.RangeLive.Index do
 | 
				
			|||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(%{"search" => search}, _session, socket) do
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session) |> display_shot_groups()}
 | 
					    {:ok, socket |> assign(search: search) |> display_shot_groups()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def mount(_params, _session, socket) do
 | 
				
			||||||
 | 
					    {:ok, socket |> assign(search: nil) |> display_shot_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -25,26 +29,46 @@ defmodule CanneryWeb.RangeLive.Index do
 | 
				
			|||||||
         %{"id" => id}
 | 
					         %{"id" => id}
 | 
				
			||||||
       ) do
 | 
					       ) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Record Shots"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:ammo_group, Ammo.get_ammo_group!(id, current_user))
 | 
					      page_title: gettext("Record Shots"),
 | 
				
			||||||
 | 
					      ammo_group: Ammo.get_ammo_group!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Edit Shot Records"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:shot_group, ActivityLog.get_shot_group!(id, current_user))
 | 
					      page_title: gettext("Edit Shot Records"),
 | 
				
			||||||
 | 
					      shot_group: ActivityLog.get_shot_group!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :new, _params) do
 | 
					  defp apply_action(socket, :new, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("New Shot Records"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:shot_group, %ShotGroup{})
 | 
					      page_title: gettext("New Shot Records"),
 | 
				
			||||||
 | 
					      shot_group: %ShotGroup{}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Shot Records"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:shot_group, nil)
 | 
					      page_title: gettext("Shot Records"),
 | 
				
			||||||
 | 
					      search: nil,
 | 
				
			||||||
 | 
					      shot_group: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> display_shot_groups()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(socket, :search, %{"search" => search}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Shot Records"),
 | 
				
			||||||
 | 
					      search: search,
 | 
				
			||||||
 | 
					      shot_group: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> display_shot_groups()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -57,6 +81,7 @@ defmodule CanneryWeb.RangeLive.Index do
 | 
				
			|||||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "toggle_staged",
 | 
					        "toggle_staged",
 | 
				
			||||||
        %{"ammo_group_id" => ammo_group_id},
 | 
					        %{"ammo_group_id" => ammo_group_id},
 | 
				
			||||||
@@ -67,79 +92,50 @@ defmodule CanneryWeb.RangeLive.Index do
 | 
				
			|||||||
    {:ok, _ammo_group} =
 | 
					    {:ok, _ammo_group} =
 | 
				
			||||||
      ammo_group |> Ammo.update_ammo_group(%{"staged" => !ammo_group.staged}, current_user)
 | 
					      ammo_group |> Ammo.update_ammo_group(%{"staged" => !ammo_group.staged}, current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prompt = dgettext("prompts", "Ammo group unstaged succesfully")
 | 
					    prompt = dgettext("prompts", "Ammo unstaged succesfully")
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :index))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :search, search_term))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec display_shot_groups(Socket.t()) :: Socket.t()
 | 
					  @spec display_shot_groups(Socket.t()) :: Socket.t()
 | 
				
			||||||
  defp display_shot_groups(%{assigns: %{current_user: current_user}} = socket) do
 | 
					  defp display_shot_groups(%{assigns: %{search: search, current_user: current_user}} = socket) do
 | 
				
			||||||
    shot_groups =
 | 
					    shot_groups =
 | 
				
			||||||
      ActivityLog.list_shot_groups(current_user) |> Repo.preload(ammo_group: :ammo_type)
 | 
					      ActivityLog.list_shot_groups(search, current_user)
 | 
				
			||||||
 | 
					      |> Repo.preload(ammo_group: :ammo_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ammo_groups = Ammo.list_staged_ammo_groups(current_user)
 | 
					    ammo_groups = Ammo.list_staged_ammo_groups(current_user)
 | 
				
			||||||
 | 
					    chart_data = shot_groups |> get_chart_data_for_shot_group()
 | 
				
			||||||
    columns = [
 | 
					 | 
				
			||||||
      %{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}
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rows =
 | 
					 | 
				
			||||||
      shot_groups
 | 
					 | 
				
			||||||
      |> Enum.map(fn shot_group -> shot_group |> get_row_data_for_shot_group(columns) end)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(ammo_groups: ammo_groups, columns: columns, rows: rows, shot_groups: shot_groups)
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					      chart_data: chart_data,
 | 
				
			||||||
 | 
					      shot_groups: shot_groups
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec get_row_data_for_shot_group(ShotGroup.t(), [map()]) :: [map()]
 | 
					  @spec get_chart_data_for_shot_group([ShotGroup.t()]) :: [map()]
 | 
				
			||||||
  defp get_row_data_for_shot_group(%{date: date} = shot_group, columns) do
 | 
					  defp get_chart_data_for_shot_group(shot_groups) do
 | 
				
			||||||
    shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type)
 | 
					    shot_groups
 | 
				
			||||||
    assigns = %{shot_group: shot_group}
 | 
					    |> 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns
 | 
					      %{
 | 
				
			||||||
    |> Enum.into(%{}, fn %{key: key} ->
 | 
					        date: date,
 | 
				
			||||||
      value =
 | 
					        count: sum,
 | 
				
			||||||
        case key do
 | 
					        label: gettext("Rounds shot: %{count}", count: sum)
 | 
				
			||||||
          "name" ->
 | 
					      }
 | 
				
			||||||
            {shot_group.ammo_group.ammo_type.name,
 | 
					 | 
				
			||||||
             live_patch(shot_group.ammo_group.ammo_type.name,
 | 
					 | 
				
			||||||
               to: Routes.ammo_group_show_path(Endpoint, :show, shot_group.ammo_group),
 | 
					 | 
				
			||||||
               class: "link"
 | 
					 | 
				
			||||||
             )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          "date" ->
 | 
					 | 
				
			||||||
            date |> display_date()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          "actions" ->
 | 
					 | 
				
			||||||
            ~H"""
 | 
					 | 
				
			||||||
            <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
              <%= live_patch to: Routes.range_index_path(Endpoint, :edit, shot_group),
 | 
					 | 
				
			||||||
                          class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                          data: [qa: "edit-#{shot_group.id}"] do %>
 | 
					 | 
				
			||||||
                <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					 | 
				
			||||||
              <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <%= link to: "#",
 | 
					 | 
				
			||||||
                    class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                    phx_click: "delete",
 | 
					 | 
				
			||||||
                    phx_value_id: shot_group.id,
 | 
					 | 
				
			||||||
                    data: [
 | 
					 | 
				
			||||||
                      confirm: dgettext("prompts", "Are you sure you want to delete this shot record?"),
 | 
					 | 
				
			||||||
                      qa: "delete-#{shot_group.id}"
 | 
					 | 
				
			||||||
                    ] do %>
 | 
					 | 
				
			||||||
                <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					 | 
				
			||||||
              <% end %>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          key ->
 | 
					 | 
				
			||||||
            shot_group |> Map.get(key |> String.to_existing_atom())
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {key, value}
 | 
					 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Enum.sort_by(fn %{date: date} -> date end, Date)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,41 +9,43 @@
 | 
				
			|||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Why not get some ready to shoot?"),
 | 
					    <.link navigate={Routes.ammo_group_index_path(Endpoint, :index)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.ammo_group_index_path(Endpoint, :index),
 | 
					      <%= dgettext("actions", "Why not get some ready to shoot?") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Stage ammo"),
 | 
					    <.link navigate={Routes.ammo_group_index_path(Endpoint, :index)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.ammo_group_index_path(Endpoint, :index),
 | 
					      <%= dgettext("actions", "Stage ammo") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= for ammo_group <- @ammo_groups do %>
 | 
					    <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
      <.ammo_group_card ammo_group={ammo_group}>
 | 
					      <%= for ammo_group <- @ammo_groups do %>
 | 
				
			||||||
        <button
 | 
					        <.ammo_group_card ammo_group={ammo_group}>
 | 
				
			||||||
          type="button"
 | 
					          <button
 | 
				
			||||||
          class="btn btn-primary"
 | 
					            type="button"
 | 
				
			||||||
          phx-click="toggle_staged"
 | 
					            class="btn btn-primary"
 | 
				
			||||||
          phx-value-ammo_group_id={ammo_group.id}
 | 
					            phx-click="toggle_staged"
 | 
				
			||||||
          data-confirm={"#{dgettext("prompts", "Are you sure you want to unstage this ammo?")}"}
 | 
					            phx-value-ammo_group_id={ammo_group.id}
 | 
				
			||||||
        >
 | 
					            data-confirm={"#{dgettext("prompts", "Are you sure you want to unstage this ammo?")}"}
 | 
				
			||||||
          <%= if ammo_group.staged,
 | 
					          >
 | 
				
			||||||
            do: gettext("Unstage from range"),
 | 
					            <%= if ammo_group.staged,
 | 
				
			||||||
            else: gettext("Stage for range") %>
 | 
					              do: dgettext("actions", "Unstage from range"),
 | 
				
			||||||
        </button>
 | 
					              else: dgettext("actions", "Stage for range") %>
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= live_patch(dgettext("actions", "Record shots"),
 | 
					          <.link
 | 
				
			||||||
          to: Routes.range_index_path(Endpoint, :add_shot_group, ammo_group),
 | 
					            patch={Routes.range_index_path(Endpoint, :add_shot_group, ammo_group)}
 | 
				
			||||||
          class: "btn btn-primary"
 | 
					            class="btn btn-primary"
 | 
				
			||||||
        ) %>
 | 
					          >
 | 
				
			||||||
      </.ammo_group_card>
 | 
					            <%= dgettext("actions", "Record shots") %>
 | 
				
			||||||
    <% end %>
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </.ammo_group_card>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= if @shot_groups |> Enum.empty?() do %>
 | 
					  <%= if @shot_groups |> Enum.empty?() and @search |> is_nil() do %>
 | 
				
			||||||
    <h1 class="title text-xl text-primary-600">
 | 
					    <h1 class="title text-xl text-primary-600">
 | 
				
			||||||
      <%= gettext("No shots recorded") %>
 | 
					      <%= gettext("No shots recorded") %>
 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
@@ -53,12 +55,76 @@
 | 
				
			|||||||
      <%= gettext("Shot log") %>
 | 
					      <%= gettext("Shot log") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <.live_component
 | 
					    <canvas
 | 
				
			||||||
      module={CanneryWeb.Components.TableComponent}
 | 
					      id="shot-log-chart"
 | 
				
			||||||
      id="shot_groups_index_table"
 | 
					      phx-hook="ShotLogChart"
 | 
				
			||||||
      columns={@columns}
 | 
					      phx-update="ignore"
 | 
				
			||||||
      rows={@rows}
 | 
					      class="max-h-72"
 | 
				
			||||||
    />
 | 
					      data-chart-data={Jason.encode!(@chart_data)}
 | 
				
			||||||
 | 
					      data-label={gettext("Rounds shot")}
 | 
				
			||||||
 | 
					      data-color={random_color()}
 | 
				
			||||||
 | 
					      aria-label={gettext("Rounds shot chart")}
 | 
				
			||||||
 | 
					      role="img"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= 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">
 | 
				
			||||||
 | 
					      <.form
 | 
				
			||||||
 | 
					        :let={f}
 | 
				
			||||||
 | 
					        for={:search}
 | 
				
			||||||
 | 
					        phx-change="search"
 | 
				
			||||||
 | 
					        phx-submit="search"
 | 
				
			||||||
 | 
					        class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
 | 
					        data-qa="shot_group_search"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					          class: "input input-primary",
 | 
				
			||||||
 | 
					          value: @search,
 | 
				
			||||||
 | 
					          phx_debounce: 300,
 | 
				
			||||||
 | 
					          placeholder: gettext("Search shot records")
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					      </.form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= if @shot_groups |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					      <h1 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("No shots recorded") %>
 | 
				
			||||||
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					      </h1>
 | 
				
			||||||
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.Components.ShotGroupTableComponent}
 | 
				
			||||||
 | 
					        id="shot_groups_index_table"
 | 
				
			||||||
 | 
					        shot_groups={@shot_groups}
 | 
				
			||||||
 | 
					        current_user={@current_user}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <:actions :let={shot_group}>
 | 
				
			||||||
 | 
					          <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.range_index_path(Endpoint, :edit, shot_group)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"edit-#{shot_group.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              href="#"
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              phx-click="delete"
 | 
				
			||||||
 | 
					              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}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </:actions>
 | 
				
			||||||
 | 
					      </.live_component>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,19 +12,44 @@ defmodule CanneryWeb.TagLive.FormComponent do
 | 
				
			|||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  @spec update(%{:tag => Tag.t(), :current_user => User.t(), optional(any) => any}, Socket.t()) ::
 | 
					  @spec update(%{:tag => Tag.t(), :current_user => User.t(), optional(any) => any}, Socket.t()) ::
 | 
				
			||||||
          {:ok, Socket.t()}
 | 
					          {:ok, Socket.t()}
 | 
				
			||||||
  def update(%{tag: tag} = assigns, socket) do
 | 
					  def update(%{tag: _tag} = assigns, socket) do
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:changeset, Tags.change_tag(tag))}
 | 
					    {:ok, socket |> assign(assigns) |> assign_changeset(%{})}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event("validate", %{"tag" => tag_params}, %{assigns: %{tag: tag}} = socket) do
 | 
					  def handle_event("validate", %{"tag" => tag_params}, socket) do
 | 
				
			||||||
    {:noreply, socket |> assign(:changeset, tag |> Tags.change_tag(tag_params))}
 | 
					    {:noreply, socket |> assign_changeset(tag_params)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event("save", %{"tag" => tag_params}, %{assigns: %{action: action}} = socket) do
 | 
					  def handle_event("save", %{"tag" => tag_params}, %{assigns: %{action: action}} = socket) do
 | 
				
			||||||
    save_tag(socket, action, tag_params)
 | 
					    save_tag(socket, action, tag_params)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp assign_changeset(
 | 
				
			||||||
 | 
					         %{assigns: %{action: action, current_user: user, tag: tag}} = socket,
 | 
				
			||||||
 | 
					         tag_params
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    changeset_action =
 | 
				
			||||||
 | 
					      case action do
 | 
				
			||||||
 | 
					        :new -> :insert
 | 
				
			||||||
 | 
					        :edit -> :update
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case action do
 | 
				
			||||||
 | 
					        :new -> tag |> Tag.create_changeset(user, tag_params)
 | 
				
			||||||
 | 
					        :edit -> tag |> Tag.update_changeset(tag_params)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeset =
 | 
				
			||||||
 | 
					      case changeset |> Changeset.apply_action(changeset_action) do
 | 
				
			||||||
 | 
					        {:ok, _data} -> changeset
 | 
				
			||||||
 | 
					        {:error, changeset} -> changeset
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(:changeset, changeset)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def render(assigns) do
 | 
					  def render(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
@@ -33,7 +58,7 @@ defmodule CanneryWeb.TagLive.FormComponent do
 | 
				
			|||||||
        <%= @title %>
 | 
					        <%= @title %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
      <.form
 | 
					      <.form
 | 
				
			||||||
        let={f}
 | 
					        :let={f}
 | 
				
			||||||
        for={@changeset}
 | 
					        for={@changeset}
 | 
				
			||||||
        id="tag-form"
 | 
					        id="tag-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"
 | 
					        class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
@@ -81,7 +106,7 @@ defmodule CanneryWeb.TagLive.FormComponent do
 | 
				
			|||||||
      case Tags.update_tag(tag, tag_params, current_user) do
 | 
					      case Tags.update_tag(tag, tag_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: tag_name}} ->
 | 
					        {:ok, %{name: tag_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} updated successfully", name: tag_name)
 | 
					          prompt = dgettext("prompts", "%{name} updated successfully", name: tag_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(:changeset, changeset)
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
@@ -99,7 +124,7 @@ defmodule CanneryWeb.TagLive.FormComponent do
 | 
				
			|||||||
      case Tags.create_tag(tag_params, current_user) do
 | 
					      case Tags.create_tag(tag_params, current_user) do
 | 
				
			||||||
        {:ok, %{name: tag_name}} ->
 | 
					        {:ok, %{name: tag_name}} ->
 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} created successfully", name: tag_name)
 | 
					          prompt = dgettext("prompts", "%{name} created successfully", name: tag_name)
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          socket |> assign(changeset: changeset)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,32 +6,56 @@ defmodule CanneryWeb.TagLive.Index do
 | 
				
			|||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  import CanneryWeb.Components.TagCard
 | 
					  import CanneryWeb.Components.TagCard
 | 
				
			||||||
  alias Cannery.{Tags, Tags.Tag}
 | 
					  alias Cannery.{Tags, Tags.Tag}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.{Endpoint, ViewHelpers}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, session, socket) do
 | 
					  def mount(%{"search" => search}, _session, socket) do
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session) |> display_tags()}
 | 
					    {:ok, socket |> assign(:search, search) |> display_tags()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def mount(_params, _session, socket) do
 | 
				
			||||||
 | 
					    {:ok, socket |> assign(:search, nil) |> display_tags()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
					  def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
				
			||||||
    {:noreply, apply_action(socket, live_action, params) |> display_tags}
 | 
					    {:noreply, apply_action(socket, live_action, params)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Edit Tag"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:tag, Tags.get_tag!(id, current_user))
 | 
					      page_title: gettext("Edit Tag"),
 | 
				
			||||||
 | 
					      tag: Tags.get_tag!(id, current_user)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :new, _params) do
 | 
					  defp apply_action(socket, :new, _params) do
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("New Tag"))
 | 
					    |> assign(
 | 
				
			||||||
    |> assign(:tag, %Tag{bg_color: Tags.random_bg_color(), text_color: "#ffffff"})
 | 
					      page_title: gettext("New Tag"),
 | 
				
			||||||
 | 
					      tag: %Tag{bg_color: ViewHelpers.random_color(), text_color: "#ffffff"}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket |> assign(:page_title, gettext("Tags")) |> assign(:tag, nil)
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Tags"),
 | 
				
			||||||
 | 
					      search: nil,
 | 
				
			||||||
 | 
					      tag: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> display_tags()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(socket, :search, %{"search" => search}) do
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("Tags"),
 | 
				
			||||||
 | 
					      search: search,
 | 
				
			||||||
 | 
					      tag: nil
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> display_tags()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -41,7 +65,16 @@ defmodule CanneryWeb.TagLive.Index do
 | 
				
			|||||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_tags()}
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_tags()}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp display_tags(%{assigns: %{current_user: current_user}} = socket) do
 | 
					  @impl true
 | 
				
			||||||
    socket |> assign(tags: Tags.list_tags(current_user))
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.tag_index_path(Endpoint, :index))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.tag_index_path(Endpoint, :search, search_term))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_tags(%{assigns: %{search: search, current_user: current_user}} = socket) do
 | 
				
			||||||
 | 
					    socket |> assign(tags: Tags.list_tags(search, current_user))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,44 +5,72 @@
 | 
				
			|||||||
  <p class="title text-md text-primary-600">
 | 
					  <p class="title text-md text-primary-600">
 | 
				
			||||||
    <%= gettext("Tags can be added to your containers to help you organize") %>
 | 
					    <%= gettext("Tags can be added to your containers to help you organize") %>
 | 
				
			||||||
  </p>
 | 
					  </p>
 | 
				
			||||||
  <%= if @tags |> Enum.empty?() do %>
 | 
					  <%= if @tags |> Enum.empty?() and @search |> is_nil() do %>
 | 
				
			||||||
    <h2 class="title text-xl text-primary-600">
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
      <%= gettext("No tags") %>
 | 
					      <%= gettext("No tags") %>
 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
    </h2>
 | 
					    </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Make your first tag!"),
 | 
					    <.link patch={Routes.tag_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.tag_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "Make your first tag!") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "New Tag"),
 | 
					    <.link patch={Routes.tag_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.tag_index_path(Endpoint, :new),
 | 
					      <%= dgettext("actions", "New Tag") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
  <div class="flex flex-row flex-wrap justify-center items-center">
 | 
					 | 
				
			||||||
    <%= for tag <- @tags do %>
 | 
					 | 
				
			||||||
      <.tag_card tag={tag}>
 | 
					 | 
				
			||||||
        <%= live_patch to: Routes.tag_index_path(Endpoint, :edit, tag),
 | 
					 | 
				
			||||||
                   class: "text-primary-600 link",
 | 
					 | 
				
			||||||
                   data: [qa: "edit-#{tag.id}"] do %>
 | 
					 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <%= link to: "#",
 | 
					  <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">
 | 
				
			||||||
             class: "text-primary-600 link",
 | 
					    <.form
 | 
				
			||||||
             phx_click: "delete",
 | 
					      :let={f}
 | 
				
			||||||
             phx_value_id: tag.id,
 | 
					      for={:search}
 | 
				
			||||||
             data: [
 | 
					      phx-change="search"
 | 
				
			||||||
               confirm: dgettext("prompts", "Are you sure you want to delete %{name}?", name: tag.name),
 | 
					      phx-submit="search"
 | 
				
			||||||
               qa: "delete-#{tag.id}"
 | 
					      class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
             ] do %>
 | 
					      data-qa="tag_search"
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					    >
 | 
				
			||||||
        <% end %>
 | 
					      <%= text_input(f, :search_term,
 | 
				
			||||||
      </.tag_card>
 | 
					        class: "input input-primary",
 | 
				
			||||||
    <% end %>
 | 
					        value: @search,
 | 
				
			||||||
 | 
					        phx_debounce: 300,
 | 
				
			||||||
 | 
					        placeholder: gettext("Search tags")
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					    </.form>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= if @tags |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					      <%= gettext("No tags") %>
 | 
				
			||||||
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					  <% else %>
 | 
				
			||||||
 | 
					    <div class="flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
 | 
					      <%= for tag <- @tags do %>
 | 
				
			||||||
 | 
					        <.tag_card tag={tag}>
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.tag_index_path(Endpoint, :edit, tag)}
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            data-qa={"edit-#{tag.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            href="#"
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            phx-click="delete"
 | 
				
			||||||
 | 
					            phx-value-id={tag.id}
 | 
				
			||||||
 | 
					            data-confirm={
 | 
				
			||||||
 | 
					              dgettext("prompts", "Are you sure you want to delete %{name}?", name: tag.name)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            data-qa={"delete-#{tag.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </.tag_card>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action in [:new, :edit] do %>
 | 
					<%= if @live_action in [:new, :edit] do %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,8 +11,17 @@ defmodule CanneryWeb.Router do
 | 
				
			|||||||
    plug :protect_from_forgery
 | 
					    plug :protect_from_forgery
 | 
				
			||||||
    plug :put_secure_browser_headers
 | 
					    plug :put_secure_browser_headers
 | 
				
			||||||
    plug :fetch_current_user
 | 
					    plug :fetch_current_user
 | 
				
			||||||
 | 
					    plug :put_user_locale, default: Application.compile_env(:gettext, :default_locale, "en_US")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Gettext.put_locale(Application.get_env(:gettext, :default_locale, "en_US"))
 | 
					  defp put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, default: default) do
 | 
				
			||||||
 | 
					    Gettext.put_locale(locale || default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, locale || default)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp put_user_locale(conn, default: default) do
 | 
				
			||||||
 | 
					    Gettext.put_locale(default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, default)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pipeline :require_admin do
 | 
					  pipeline :require_admin do
 | 
				
			||||||
@@ -51,42 +60,52 @@ defmodule CanneryWeb.Router do
 | 
				
			|||||||
    put "/users/settings", UserSettingsController, :update
 | 
					    put "/users/settings", UserSettingsController, :update
 | 
				
			||||||
    delete "/users/settings/:id", UserSettingsController, :delete
 | 
					    delete "/users/settings/:id", UserSettingsController, :delete
 | 
				
			||||||
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
 | 
					    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
 | 
				
			||||||
 | 
					    get "/export/:mode", ExportController, :export
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/tags", TagLive.Index, :index
 | 
					    live "/tags", TagLive.Index, :index
 | 
				
			||||||
    live "/tags/new", TagLive.Index, :new
 | 
					    live "/tags/new", TagLive.Index, :new
 | 
				
			||||||
    live "/tags/:id/edit", TagLive.Index, :edit
 | 
					    live "/tags/edit/:id", TagLive.Index, :edit
 | 
				
			||||||
 | 
					    live "/tags/search/:search", TagLive.Index, :search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/ammo_types", AmmoTypeLive.Index, :index
 | 
					    live "/catalog", AmmoTypeLive.Index, :index
 | 
				
			||||||
    live "/ammo_types/new", AmmoTypeLive.Index, :new
 | 
					    live "/catalog/new", AmmoTypeLive.Index, :new
 | 
				
			||||||
    live "/ammo_types/:id/edit", AmmoTypeLive.Index, :edit
 | 
					    live "/catalog/clone/:id", AmmoTypeLive.Index, :clone
 | 
				
			||||||
 | 
					    live "/catalog/edit/:id", AmmoTypeLive.Index, :edit
 | 
				
			||||||
 | 
					    live "/catalog/search/:search", AmmoTypeLive.Index, :search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/ammo_types/:id", AmmoTypeLive.Show, :show
 | 
					    live "/type/:id", AmmoTypeLive.Show, :show
 | 
				
			||||||
    live "/ammo_types/:id/show/edit", AmmoTypeLive.Show, :edit
 | 
					    live "/type/:id/edit", AmmoTypeLive.Show, :edit
 | 
				
			||||||
 | 
					    live "/type/:id/table", AmmoTypeLive.Show, :table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/containers", ContainerLive.Index, :index
 | 
					    live "/containers", ContainerLive.Index, :index
 | 
				
			||||||
    live "/containers/new", ContainerLive.Index, :new
 | 
					    live "/containers/new", ContainerLive.Index, :new
 | 
				
			||||||
    live "/containers/:id/edit", ContainerLive.Index, :edit
 | 
					    live "/containers/edit/:id", ContainerLive.Index, :edit
 | 
				
			||||||
    live "/containers/:id/edit_tags", ContainerLive.Index, :edit_tags
 | 
					    live "/containers/clone/:id", ContainerLive.Index, :clone
 | 
				
			||||||
 | 
					    live "/containers/edit_tags/:id", ContainerLive.Index, :edit_tags
 | 
				
			||||||
 | 
					    live "/containers/search/:search", ContainerLive.Index, :search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/containers/:id", ContainerLive.Show, :show
 | 
					    live "/container/:id", ContainerLive.Show, :show
 | 
				
			||||||
    live "/containers/:id/show/edit", ContainerLive.Show, :edit
 | 
					    live "/container/edit/:id", ContainerLive.Show, :edit
 | 
				
			||||||
    live "/containers/:id/show/edit_tags", ContainerLive.Show, :edit_tags
 | 
					    live "/container/edit_tags/:id", ContainerLive.Show, :edit_tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/ammo_groups", AmmoGroupLive.Index, :index
 | 
					    live "/ammo", AmmoGroupLive.Index, :index
 | 
				
			||||||
    live "/ammo_groups/new", AmmoGroupLive.Index, :new
 | 
					    live "/ammo/new", AmmoGroupLive.Index, :new
 | 
				
			||||||
    live "/ammo_groups/:id/edit", AmmoGroupLive.Index, :edit
 | 
					    live "/ammo/edit/:id", AmmoGroupLive.Index, :edit
 | 
				
			||||||
    live "/ammo_groups/:id/add_shot_group", AmmoGroupLive.Index, :add_shot_group
 | 
					    live "/ammo/clone/:id", AmmoGroupLive.Index, :clone
 | 
				
			||||||
    live "/ammo_groups/:id/move", AmmoGroupLive.Index, :move
 | 
					    live "/ammo/add_shot_group/:id", AmmoGroupLive.Index, :add_shot_group
 | 
				
			||||||
 | 
					    live "/ammo/move/:id", AmmoGroupLive.Index, :move
 | 
				
			||||||
 | 
					    live "/ammo/search/:search", AmmoGroupLive.Index, :search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/ammo_groups/:id", AmmoGroupLive.Show, :show
 | 
					    live "/ammo/show/:id", AmmoGroupLive.Show, :show
 | 
				
			||||||
    live "/ammo_groups/:id/show/edit", AmmoGroupLive.Show, :edit
 | 
					    live "/ammo/show/edit/:id", AmmoGroupLive.Show, :edit
 | 
				
			||||||
    live "/ammo_groups/:id/show/add_shot_group", AmmoGroupLive.Show, :add_shot_group
 | 
					    live "/ammo/show/add_shot_group/:id", AmmoGroupLive.Show, :add_shot_group
 | 
				
			||||||
    live "/ammo_groups/:id/show/move", AmmoGroupLive.Show, :move
 | 
					    live "/ammo/show/move/:id", AmmoGroupLive.Show, :move
 | 
				
			||||||
    live "/ammo_groups/:id/show/:shot_group_id/edit", AmmoGroupLive.Show, :edit_shot_group
 | 
					    live "/ammo/show/:id/edit/:shot_group_id", AmmoGroupLive.Show, :edit_shot_group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    live "/range", RangeLive.Index, :index
 | 
					    live "/range", RangeLive.Index, :index
 | 
				
			||||||
    live "/range/:id/edit", RangeLive.Index, :edit
 | 
					    live "/range/edit/:id", RangeLive.Index, :edit
 | 
				
			||||||
    live "/range/:id/add_shot_group", RangeLive.Index, :add_shot_group
 | 
					    live "/range/add_shot_group/:id", RangeLive.Index, :add_shot_group
 | 
				
			||||||
 | 
					    live "/range/search/:search", RangeLive.Index, :search
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scope "/", CanneryWeb do
 | 
					  scope "/", CanneryWeb do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,33 +22,21 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </header>
 | 
					  </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="mx-4 sm:mx-8 md:mx-16">
 | 
					  <div class="mx-4 sm:mx-8 md:mx-16 flex flex-col justify-center items-stretch">
 | 
				
			||||||
    <%= @inner_content %>
 | 
					    <%= @inner_content %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</main>
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div
 | 
					 | 
				
			||||||
  id="loading"
 | 
					 | 
				
			||||||
  class="fixed opacity-0 top-0 left-0 w-screen h-screen bg-white z-50
 | 
					 | 
				
			||||||
  flex flex-col justify-center items-center space-y-4
 | 
					 | 
				
			||||||
  transition-opacity ease-in-out duration-500"
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
  <h1 class="title text-2xl title-primary-500">
 | 
					 | 
				
			||||||
    <%= gettext("Loading...") %>
 | 
					 | 
				
			||||||
  </h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <i class="fas fa-3x fa-spin fa-cog"></i>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
  id="disconnect"
 | 
					  id="disconnect"
 | 
				
			||||||
  class="fixed opacity-0 top-0 left-0 w-screen h-screen bg-white z-50
 | 
					  class="z-50 fixed opacity-0 bottom-12 right-12 px-8 py-4 w-max h-max
 | 
				
			||||||
  flex flex-col justify-center items-center space-y-4
 | 
					  border border-primary-200 shadow-lg rounded-lg bg-white
 | 
				
			||||||
  transition-opacity ease-in-out duration-500"
 | 
					  flex justify-center items-center space-x-4
 | 
				
			||||||
 | 
					  transition-opacity ease-in-out duration-500 delay-[2000ms]"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
  <h1 class="title text-2xl title-primary-500">
 | 
					  <i class="fas fa-fade text-md fa-satellite-dish"></i>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <h1 class="title text-md title-primary-500">
 | 
				
			||||||
    <%= gettext("Reconnecting...") %>
 | 
					    <%= gettext("Reconnecting...") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					 | 
				
			||||||
  <i class="fas fa-3x fa-fade fa-satellite-dish"></i>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,14 @@
 | 
				
			|||||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <%= csrf_meta_tag() %>
 | 
					    <%= csrf_meta_tag() %>
 | 
				
			||||||
    <%= if(assigns |> Map.has_key?(:page_title), do: @page_title, else: "Cannery")
 | 
					    <link
 | 
				
			||||||
    |> live_title_tag(suffix: " | Cannery") %>
 | 
					      rel="shortcut icon"
 | 
				
			||||||
 | 
					      type="image/jpg"
 | 
				
			||||||
 | 
					      href={Routes.static_path(@conn, "/images/cannery.svg")}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <.live_title suffix=" | Cannery">
 | 
				
			||||||
 | 
					      <%= assigns[:page_title] || "Cannery" %>
 | 
				
			||||||
 | 
					    </.live_title>
 | 
				
			||||||
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
 | 
					    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
 | 
				
			||||||
    <script
 | 
					    <script
 | 
				
			||||||
      defer
 | 
					      defer
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,14 +22,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <%= if Accounts.allow_registration?() do %>
 | 
				
			||||||
      <%= link(dgettext("actions", "Register"),
 | 
					      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
        to: Routes.user_registration_path(@conn, :new),
 | 
					        <%= dgettext("actions", "Register") %>
 | 
				
			||||||
        class: "btn btn-primary"
 | 
					      </.link>
 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
    <%= link(dgettext("actions", "Log in"),
 | 
					    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.user_session_path(@conn, :new),
 | 
					      <%= dgettext("actions", "Log in") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,19 +30,26 @@
 | 
				
			|||||||
    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :password, "col-span-3") %>
 | 
					    <%= error_tag(f, :password, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
 | 
					    <%= select(
 | 
				
			||||||
 | 
					      f,
 | 
				
			||||||
 | 
					      :locale,
 | 
				
			||||||
 | 
					      [{gettext("English"), "en_US"}, {gettext("German"), "de"}, {gettext("French"), "fr"}],
 | 
				
			||||||
 | 
					      class: "input input-primary col-span-2"
 | 
				
			||||||
 | 
					    ) %>
 | 
				
			||||||
 | 
					    <%= error_tag(f, :locale) %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3") %>
 | 
					    <%= submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3") %>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= link(dgettext("actions", "Log in"),
 | 
					    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.user_session_path(@conn, :new),
 | 
					      <%= dgettext("actions", "Log in") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
    <%= link(dgettext("actions", "Forgot your password?"),
 | 
					      <%= dgettext("actions", "Forgot your password?") %>
 | 
				
			||||||
      to: Routes.user_reset_password_path(@conn, :new),
 | 
					    </.link>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,14 +40,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <%= if Accounts.allow_registration?() do %>
 | 
				
			||||||
      <%= link(dgettext("actions", "Register"),
 | 
					      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
        to: Routes.user_registration_path(@conn, :new),
 | 
					        <%= dgettext("actions", "Register") %>
 | 
				
			||||||
        class: "btn btn-primary"
 | 
					      </.link>
 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
    <%= link(dgettext("actions", "Log in"),
 | 
					    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.user_session_path(@conn, :new),
 | 
					      <%= dgettext("actions", "Log in") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,14 +22,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <%= if Accounts.allow_registration?() do %>
 | 
				
			||||||
      <%= link(dgettext("actions", "Register"),
 | 
					      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
        to: Routes.user_registration_path(@conn, :new),
 | 
					        <%= dgettext("actions", "Register") %>
 | 
				
			||||||
        class: "btn btn-primary"
 | 
					      </.link>
 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
    <%= link(dgettext("actions", "Log in"),
 | 
					    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.user_session_path(@conn, :new),
 | 
					      <%= dgettext("actions", "Log in") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,14 +37,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <%= if Accounts.allow_registration?() do %>
 | 
				
			||||||
      <%= link(dgettext("actions", "Register"),
 | 
					      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
        to: Routes.user_registration_path(@conn, :new),
 | 
					        <%= dgettext("actions", "Register") %>
 | 
				
			||||||
        class: "btn btn-primary"
 | 
					      </.link>
 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
    <%= link(dgettext("actions", "Forgot your password?"),
 | 
					    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
				
			||||||
      to: Routes.user_reset_password_path(@conn, :new),
 | 
					      <%= dgettext("actions", "Forgot your password?") %>
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					    </.link>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,16 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center text-center space-y-4">
 | 
				
			||||||
  <h1 class="pb-4 title text-primary-600 text-xl">
 | 
					  <h1 class="pb-4 title text-primary-600 text-xl">
 | 
				
			||||||
    <%= gettext("Settings") %>
 | 
					    <%= gettext("Settings") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= form_for @email_changeset,
 | 
					  <.form
 | 
				
			||||||
           Routes.user_settings_path(@conn, :update),
 | 
					    :let={f}
 | 
				
			||||||
           [
 | 
					    for={@email_changeset}
 | 
				
			||||||
             class:
 | 
					    action={Routes.user_settings_path(@conn, :update)}
 | 
				
			||||||
               "flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
           ],
 | 
					  >
 | 
				
			||||||
           fn f -> %>
 | 
					 | 
				
			||||||
    <h3 class="title text-primary-600 text-lg col-span-3">
 | 
					    <h3 class="title text-primary-600 text-lg col-span-3">
 | 
				
			||||||
      <%= dgettext("actions", "Change email") %>
 | 
					      <%= dgettext("actions", "Change email") %>
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
@@ -45,17 +44,16 @@
 | 
				
			|||||||
    <%= submit(dgettext("actions", "Change email"),
 | 
					    <%= submit(dgettext("actions", "Change email"),
 | 
				
			||||||
      class: "mx-auto btn btn-primary col-span-3"
 | 
					      class: "mx-auto btn btn-primary col-span-3"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
  <% end %>
 | 
					  </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= form_for @password_changeset,
 | 
					  <.form
 | 
				
			||||||
           Routes.user_settings_path(@conn, :update),
 | 
					    :let={f}
 | 
				
			||||||
           [
 | 
					    for={@password_changeset}
 | 
				
			||||||
             class:
 | 
					    action={Routes.user_settings_path(@conn, :update)}
 | 
				
			||||||
               "flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
           ],
 | 
					  >
 | 
				
			||||||
           fn f -> %>
 | 
					 | 
				
			||||||
    <h3 class="title text-primary-600 text-lg col-span-3">
 | 
					    <h3 class="title text-primary-600 text-lg col-span-3">
 | 
				
			||||||
      <%= dgettext("actions", "Change password") %>
 | 
					      <%= dgettext("actions", "Change password") %>
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
@@ -101,14 +99,67 @@
 | 
				
			|||||||
    <%= submit(dgettext("actions", "Change password"),
 | 
					    <%= submit(dgettext("actions", "Change password"),
 | 
				
			||||||
      class: "mx-auto btn btn-primary col-span-3"
 | 
					      class: "mx-auto btn btn-primary col-span-3"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
  <% end %>
 | 
					  </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= link(dgettext("actions", "Delete User"),
 | 
					  <.form
 | 
				
			||||||
    to: Routes.user_settings_path(@conn, :delete, @current_user),
 | 
					    :let={f}
 | 
				
			||||||
    method: :delete,
 | 
					    for={@locale_changeset}
 | 
				
			||||||
    class: "btn btn-alert",
 | 
					    action={Routes.user_settings_path(@conn, :update)}
 | 
				
			||||||
    data: [confirm: dgettext("prompts", "Are you sure you want to delete your account?")]
 | 
					    class="flex flex-col space-y-4 justify-center items-center"
 | 
				
			||||||
  ) %>
 | 
					  >
 | 
				
			||||||
 | 
					    <h3 class="title text-primary-600 text-lg">
 | 
				
			||||||
 | 
					      <%= dgettext("actions", "Change Language") %>
 | 
				
			||||||
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= if @locale_changeset.action && not @locale_changeset.valid? do %>
 | 
				
			||||||
 | 
					      <div class="alert alert-danger">
 | 
				
			||||||
 | 
					        <p>
 | 
				
			||||||
 | 
					          <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= hidden_input(f, :action, name: "action", value: "update_locale") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= select(
 | 
				
			||||||
 | 
					      f,
 | 
				
			||||||
 | 
					      :locale,
 | 
				
			||||||
 | 
					      [
 | 
				
			||||||
 | 
					        {gettext("English"), "en_US"},
 | 
				
			||||||
 | 
					        {gettext("German"), "de"},
 | 
				
			||||||
 | 
					        {gettext("French"), "fr"},
 | 
				
			||||||
 | 
					        {gettext("Spanish"), "es"}
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      class: "mx-2 my-1 min-w-md input input-primary"
 | 
				
			||||||
 | 
					    ) %>
 | 
				
			||||||
 | 
					    <%= error_tag(f, :locale) %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= submit(dgettext("actions", "Change language"),
 | 
				
			||||||
 | 
					      class: "whitespace-nowrap mx-auto btn btn-primary",
 | 
				
			||||||
 | 
					      data: [qa: dgettext("prompts", "Are you sure you want to change your language?")]
 | 
				
			||||||
 | 
					    ) %>
 | 
				
			||||||
 | 
					  </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="flex justify-center items-center">
 | 
				
			||||||
 | 
					    <.link
 | 
				
			||||||
 | 
					      href={Routes.export_path(@conn, :export, :json)}
 | 
				
			||||||
 | 
					      class="mx-4 my-2 btn btn-primary"
 | 
				
			||||||
 | 
					      target="_blank"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= dgettext("actions", "Export Data as JSON") %>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.link
 | 
				
			||||||
 | 
					      href={Routes.user_settings_path(@conn, :delete, @current_user)}
 | 
				
			||||||
 | 
					      method={:delete}
 | 
				
			||||||
 | 
					      class="mx-4 my-2 btn btn-alert"
 | 
				
			||||||
 | 
					      data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= dgettext("actions", "Delete User") %>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ defmodule CanneryWeb.ErrorHelpers do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use Phoenix.HTML
 | 
					  use Phoenix.HTML
 | 
				
			||||||
  import Phoenix.LiveView.Helpers
 | 
					  import Phoenix.Component
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
  alias Phoenix.{HTML.Form, LiveView.Rendered}
 | 
					  alias Phoenix.{HTML.Form, LiveView.Rendered}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ defmodule CanneryWeb.ViewHelpers do
 | 
				
			|||||||
  :view`
 | 
					  :view`
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import Phoenix.LiveView.Helpers
 | 
					  import Phoenix.Component
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @id_length 16
 | 
					  @id_length 16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,11 +23,16 @@ defmodule CanneryWeb.ViewHelpers do
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <time id={@id} datetime={@datetime} x-data={"{
 | 
					    <time
 | 
				
			||||||
 | 
					      id={@id}
 | 
				
			||||||
 | 
					      datetime={@datetime}
 | 
				
			||||||
 | 
					      x-data={"{
 | 
				
			||||||
        date:
 | 
					        date:
 | 
				
			||||||
          Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
 | 
					          Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
 | 
				
			||||||
            .format(new Date(\"#{@datetime}\"))
 | 
					            .format(new Date(\"#{@datetime}\"))
 | 
				
			||||||
      }"} x-text="date">
 | 
					      }"}
 | 
				
			||||||
 | 
					      x-text="date"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <%= @datetime %>
 | 
					      <%= @datetime %>
 | 
				
			||||||
    </time>
 | 
					    </time>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -47,10 +52,15 @@ defmodule CanneryWeb.ViewHelpers do
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <time id={@id} datetime={@date} x-data={"{
 | 
					    <time
 | 
				
			||||||
 | 
					      id={@id}
 | 
				
			||||||
 | 
					      datetime={@date}
 | 
				
			||||||
 | 
					      x-data={"{
 | 
				
			||||||
        date:
 | 
					        date:
 | 
				
			||||||
          Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'}).format(new Date(\"#{@date}\"))
 | 
					          Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'}).format(new Date(\"#{@date}\"))
 | 
				
			||||||
      }"} x-text="date">
 | 
					      }"}
 | 
				
			||||||
 | 
					      x-text="date"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <%= @date %>
 | 
					      <%= @date %>
 | 
				
			||||||
    </time>
 | 
					    </time>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -65,4 +75,18 @@ defmodule CanneryWeb.ViewHelpers do
 | 
				
			|||||||
      if(Application.get_env(:cannery, CanneryWeb.ViewHelpers)[:shibao_mode], do: "q_q", else: "😔")
 | 
					      if(Application.get_env(:cannery, CanneryWeb.ViewHelpers)[:shibao_mode], do: "q_q", else: "😔")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def display_emoji(other_emoji), do: other_emoji
 | 
					  def display_emoji(other_emoji), do: other_emoji
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Get a random color in `#ffffff` hex format
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> random_color()
 | 
				
			||||||
 | 
					      "#cc0066"
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec random_color() :: <<_::7>>
 | 
				
			||||||
 | 
					  def random_color do
 | 
				
			||||||
 | 
					    ["#cc0066", "#ff6699", "#6666ff", "#0066cc", "#00cc66", "#669900", "#ff9900", "#996633"]
 | 
				
			||||||
 | 
					    |> Enum.random()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								mix.exs
									
									
									
									
									
								
							@@ -4,15 +4,16 @@ defmodule Cannery.MixProject do
 | 
				
			|||||||
  def project do
 | 
					  def project do
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
      app: :cannery,
 | 
					      app: :cannery,
 | 
				
			||||||
      version: "0.5.1",
 | 
					      version: "0.8.1",
 | 
				
			||||||
      elixir: "1.13.4",
 | 
					      elixir: "1.14.1",
 | 
				
			||||||
      elixirc_paths: elixirc_paths(Mix.env()),
 | 
					      elixirc_paths: elixirc_paths(Mix.env()),
 | 
				
			||||||
      compilers: [:gettext] ++ Mix.compilers(),
 | 
					      compilers: Mix.compilers(),
 | 
				
			||||||
      start_permanent: Mix.env() == :prod,
 | 
					      start_permanent: Mix.env() == :prod,
 | 
				
			||||||
      aliases: aliases(),
 | 
					      aliases: aliases(),
 | 
				
			||||||
      deps: deps(),
 | 
					      deps: deps(),
 | 
				
			||||||
      dialyzer: [plt_add_apps: [:ex_unit]],
 | 
					      dialyzer: [plt_add_apps: [:ex_unit]],
 | 
				
			||||||
      consolidate_protocols: Mix.env() not in [:dev, :test],
 | 
					      consolidate_protocols: Mix.env() not in [:dev, :test],
 | 
				
			||||||
 | 
					      preferred_cli_env: ["test.all": :test],
 | 
				
			||||||
      # ExDoc
 | 
					      # ExDoc
 | 
				
			||||||
      name: "Cannery",
 | 
					      name: "Cannery",
 | 
				
			||||||
      source_url: "https://gitea.bubbletea.dev/shibao/cannery",
 | 
					      source_url: "https://gitea.bubbletea.dev/shibao/cannery",
 | 
				
			||||||
@@ -47,11 +48,11 @@ defmodule Cannery.MixProject do
 | 
				
			|||||||
  defp deps do
 | 
					  defp deps do
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
      {:bcrypt_elixir, "~> 2.0"},
 | 
					      {:bcrypt_elixir, "~> 2.0"},
 | 
				
			||||||
      {:phoenix, "~> 1.6"},
 | 
					      {:phoenix, "~> 1.6.0"},
 | 
				
			||||||
      {:phoenix_ecto, "~> 4.4"},
 | 
					      {:phoenix_ecto, "~> 4.4"},
 | 
				
			||||||
      {:phoenix_html, "~> 3.0"},
 | 
					      {:phoenix_html, "~> 3.0"},
 | 
				
			||||||
      {:phoenix_live_reload, "~> 1.2", only: :dev},
 | 
					      {:phoenix_live_reload, "~> 1.2", only: :dev},
 | 
				
			||||||
      {:phoenix_live_view, "~> 0.17"},
 | 
					      {:phoenix_live_view, "~> 0.18.0"},
 | 
				
			||||||
      {:phoenix_view, "~> 1.1"},
 | 
					      {:phoenix_view, "~> 1.1"},
 | 
				
			||||||
      {:phoenix_live_dashboard, "~> 0.6"},
 | 
					      {:phoenix_live_dashboard, "~> 0.6"},
 | 
				
			||||||
      {:ecto_sql, "~> 3.6"},
 | 
					      {:ecto_sql, "~> 3.6"},
 | 
				
			||||||
@@ -85,12 +86,19 @@ defmodule Cannery.MixProject do
 | 
				
			|||||||
      setup: ["deps.get", "compile", "ecto.setup", "cmd npm install --prefix assets"],
 | 
					      setup: ["deps.get", "compile", "ecto.setup", "cmd npm install --prefix assets"],
 | 
				
			||||||
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
 | 
					      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
 | 
				
			||||||
      "ecto.reset": ["ecto.drop", "ecto.setup"],
 | 
					      "ecto.reset": ["ecto.drop", "ecto.setup"],
 | 
				
			||||||
      format: ["cmd npm run format --prefix assets", "format", "gettext.extract"],
 | 
					      "format.all": [
 | 
				
			||||||
      test: [
 | 
					        "cmd npm run format --prefix assets",
 | 
				
			||||||
 | 
					        "format",
 | 
				
			||||||
 | 
					        "gettext.extract --merge",
 | 
				
			||||||
 | 
					        "gettext.merge --no-fuzzy priv/gettext"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "test.all": [
 | 
				
			||||||
        "cmd npm run test --prefix assets",
 | 
					        "cmd npm run test --prefix assets",
 | 
				
			||||||
        "dialyzer",
 | 
					        "dialyzer",
 | 
				
			||||||
        "credo --strict",
 | 
					        "credo --strict",
 | 
				
			||||||
        "format --check-formatted",
 | 
					        "format --check-formatted",
 | 
				
			||||||
 | 
					        "gettext.extract --check-up-to-date",
 | 
				
			||||||
 | 
					        "ecto.drop --quiet",
 | 
				
			||||||
        "ecto.create --quiet",
 | 
					        "ecto.create --quiet",
 | 
				
			||||||
        "ecto.migrate --quiet",
 | 
					        "ecto.migrate --quiet",
 | 
				
			||||||
        "test"
 | 
					        "test"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								mix.lock
									
									
									
									
									
								
							@@ -1,56 +1,52 @@
 | 
				
			|||||||
%{
 | 
					%{
 | 
				
			||||||
  "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, "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"},
 | 
				
			||||||
  "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
 | 
					  "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
 | 
				
			||||||
  "castore": {:hex, :castore, "0.1.14", "3f6d7c7c1574c402fef29559d3f1a7389ba3524bc6a090a5e9e6abc3af65dcca", [:mix], [], "hexpm", "b34af542eadb727e6c8b37fdf73e18b2e02eb483a4ea0b52fd500bc23f052b7b"},
 | 
					  "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
 | 
				
			||||||
  "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
 | 
					  "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
 | 
				
			||||||
  "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
 | 
					  "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
 | 
				
			||||||
  "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
 | 
					  "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
 | 
				
			||||||
  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
 | 
					  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
 | 
				
			||||||
  "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
 | 
					  "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
 | 
				
			||||||
  "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"},
 | 
					  "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
 | 
				
			||||||
  "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
 | 
					  "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"},
 | 
					  "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
 | 
				
			||||||
  "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
 | 
					  "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.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"},
 | 
					  "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
 | 
				
			||||||
  "ecto": {:hex, :ecto, "3.7.2", "44c034f88e1980754983cc4400585970b4206841f6f3780967a65a9150ef09a8", [: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", "a600da5772d1c31abbf06f3e4a1ffb150e74ed3e2aa92ff3cee95901657a874e"},
 | 
					  "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.4", "5d43fd088d39a158c860b17e8d210669587f63ec89ea122a4654861c8c6e2db4", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.15.7", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "311db02f1b772e3d0dc7f56a05044b5e1499d78ed6abf38885e1ca70059449e5"},
 | 
					  "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.7.2", "55c60aa3a06168912abf145c6df38b0295c34118c3624cf7a6977cd6ce043081", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 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", "3c218ea62f305dcaef0b915fb56583195e7b91c91dcfb006ba1f669bfacbff2a"},
 | 
					  "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.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
 | 
					  "elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"},
 | 
				
			||||||
  "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
 | 
					  "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
 | 
				
			||||||
  "esbuild": {:hex, :esbuild, "0.4.0", "9f17db148aead4cf1e6e6a584214357287a93407b5fb51a031f122b61385d4c2", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b61e4e6b92ffe45e4ee4755a22de6211a67c67987dc02afb35a425a0add1d447"},
 | 
					  "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"},
 | 
				
			||||||
  "ex_doc": {:hex, :ex_doc, "0.28.3", "6eea2f69995f5fba94cd6dd398df369fe4e777a47cd887714a0976930615c9e6", [: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", "05387a6a2655b5f9820f3f627450ed20b4325c25977b2ee69bed90af6688e718"},
 | 
					  "expo": {:hex, :expo, "0.3.0", "13127c1d5f653b2927f2616a4c9ace5ae372efd67c7c2693b87fd0fdc30c6feb", [:mix], [], "hexpm", "fb3cd4bf012a77bc1608915497dae2ff684a06f0fa633c7afa90c4d72b881823"},
 | 
				
			||||||
  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
 | 
					  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
 | 
				
			||||||
  "floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"},
 | 
					  "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
 | 
				
			||||||
  "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
 | 
					  "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.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
 | 
					  "gettext": {:hex, :gettext, "0.22.0", "a25d71ec21b1848957d9207b81fd61cb25161688d282d58bdafef74c2270bdc4", [:mix], [{:expo, "~> 0.3.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "cb0675141576f73720c8e49b4f0fd3f2c69f0cd8c218202724d4aebab8c70ace"},
 | 
				
			||||||
  "heex_formatter": {:git, "https://github.com/feliperenan/heex_formatter.git", "dfefc9ae267fb0874c287ceb6c47dda106c59552", []},
 | 
					  "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
 | 
				
			||||||
  "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
 | 
					 | 
				
			||||||
  "hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
 | 
					 | 
				
			||||||
  "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
 | 
					 | 
				
			||||||
  "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": {: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_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"},
 | 
					  "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.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
 | 
					  "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
 | 
				
			||||||
  "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
 | 
					  "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
 | 
				
			||||||
  "oban": {:hex, :oban, "2.11.3", "f431f2f0c251b8490a7fa00d2cce7197a1cf4d3f04a3305e80411f083392998f", [: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", "26529da52bfca27740c984bfc70e7f954d6411ceeae0c7c94d2c8aa7c00b513d"},
 | 
					  "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.7", "f1de32418bbbcd471f4fe74d3860ee9c8e8c6c36a0ec173be8ff468a5d72ac90", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.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", "b354a4f11d9a2f3a380fb731042dae064f22d7aed8c7e7c024a2459f12994aad"},
 | 
					  "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"},
 | 
				
			||||||
  "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_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_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.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [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.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"},
 | 
					  "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_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [: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", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
 | 
					  "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.17.9", "36b5aa812bc3ccd64c9630f6b3234d9ea21105493237e927aae19d0ba758f0db", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [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", "f7ebc3e0ba0c5f6b6996ed6c901ddbfdaba59a6d09b569e7cb2f2f7d693b4455"},
 | 
					  "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_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
 | 
					  "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
 | 
				
			||||||
  "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.1", "0db6eb6405a6b06cae4fdf4144659b3f4fee4553e2856fe8a53ba12e9fb21a74", [: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", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e34890004baec08f0fa12bd8c77bf64bfb4156b84a07fb79da9322fa94bc3781"},
 | 
					  "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_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"},
 | 
				
			||||||
  "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [: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", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
 | 
					  "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.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
 | 
					  "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.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
 | 
					  "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
 | 
				
			||||||
  "postgrex": {:hex, :postgrex, "0.16.2", "0f83198d0e73a36e8d716b90f45f3bde75b5eebf4ade4f43fa1f88c90a812f74", [: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]}], "hexpm", "a9ea589754d9d4d076121090662b7afe155b374897a6550eb288f11d755acfa0"},
 | 
					  "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"},
 | 
					  "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
 | 
				
			||||||
  "set_locale": {:hex, :set_locale, "0.2.9", "33350ba3c66f1c560dffc43019eea4b573f91c5cbe3e461fe0e5395d2d6ba2c3", [:mix], [{:gettext, "~>0.14", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix, ">1.3.0", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "e46348b64b7c5d725d0c90a6524942a19b65e6ce27372ddf9a727dfb64ba236c"},
 | 
					  "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"},
 | 
				
			||||||
  "swoosh": {:hex, :swoosh, "1.6.4", "ce3a4bf3e5276fd114178ebc5ed072ee0c177a7b3a09e5992aa005778ac143c2", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, 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", "ad4c8b534812433730b6241a1d9df38b1da75fdfa340f51887a31d7e9343fffe"},
 | 
					 | 
				
			||||||
  "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
 | 
					  "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
 | 
				
			||||||
  "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
 | 
					  "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
 | 
				
			||||||
  "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
 | 
					  "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
 | 
				
			||||||
  "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
 | 
					  "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,194 +10,242 @@
 | 
				
			|||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
msgstr ""
 | 
					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:40
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.ex:44
 | 
					 | 
				
			||||||
msgid "Add Ammo"
 | 
					msgid "Add Ammo"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:36
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:24
 | 
					 | 
				
			||||||
msgid "Add your first box!"
 | 
					msgid "Add your first box!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/container_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/container_live/index.html.heex:12
 | 
					 | 
				
			||||||
msgid "Add your first container!"
 | 
					msgid "Add your first container!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_type_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:12
 | 
					 | 
				
			||||||
msgid "Add your first type!"
 | 
					msgid "Add your first type!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:15
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:44
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:16
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:45
 | 
					 | 
				
			||||||
msgid "Change email"
 | 
					msgid "Change email"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:58
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:99
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:60
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:101
 | 
					 | 
				
			||||||
msgid "Change password"
 | 
					msgid "Change password"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/invite_live/index.html.heex:17
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:17
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Create Invite"
 | 
					msgid "Create Invite"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:162
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:108
 | 
					 | 
				
			||||||
msgid "Delete User"
 | 
					msgid "Delete User"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:52
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:43
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:45
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:45
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Forgot your password?"
 | 
					msgid "Forgot your password?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/index.html.heex:12
 | 
					 | 
				
			||||||
msgid "Invite someone new!"
 | 
					msgid "Invite someone new!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#: lib/cannery_web/components/topbar.ex:137
 | 
				
			||||||
#: lib/cannery_web/components/topbar.ex:106
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:30
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:30
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:39
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:49
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:48
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:48
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:30
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:30
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:33
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:33
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Log in"
 | 
					msgid "Log in"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/tag_live/index.html.heex:15
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/tag_live/index.html.heex:14
 | 
					 | 
				
			||||||
msgid "Make your first tag!"
 | 
					msgid "Make your first tag!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:42
 | 
					 | 
				
			||||||
msgid "New Ammo group"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:17
 | 
					#: lib/cannery_web/live/ammo_type_live/index.html.heex:17
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "New Ammo type"
 | 
					msgid "New Ammo type"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/container_live/index.html.heex:17
 | 
					#: lib/cannery_web/live/container_live/index.html.heex:17
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "New Container"
 | 
					msgid "New Container"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/tag_live/index.html.heex:19
 | 
					#: lib/cannery_web/live/tag_live/index.html.heex:19
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "New Tag"
 | 
					msgid "New Tag"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#: lib/cannery_web/components/topbar.ex:128
 | 
				
			||||||
#: lib/cannery_web/components/topbar.ex:99
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:26
 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:25
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:33
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:42
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:43
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:44
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:25
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:26
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:40
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:41
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Register"
 | 
					msgid "Register"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:16
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:16
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Resend confirmation instructions"
 | 
					msgid "Resend confirmation instructions"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:3
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:34
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:34
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Reset password"
 | 
					msgid "Reset password"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#: lib/cannery_web/components/add_shot_group_component.html.heex:53
 | 
				
			||||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:46
 | 
					#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:81
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:73
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:156
 | 
					#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:156
 | 
				
			||||||
#: lib/cannery_web/live/container_live/form_component.html.heex:50
 | 
					#: lib/cannery_web/live/container_live/form_component.html.heex:50
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:28
 | 
					#: lib/cannery_web/live/invite_live/form_component.html.heex:31
 | 
				
			||||||
#: lib/cannery_web/live/range_live/form_component.html.heex:40
 | 
					#: lib/cannery_web/live/range_live/form_component.html.heex:40
 | 
				
			||||||
#: lib/cannery_web/live/tag_live/form_component.ex:66
 | 
					#: lib/cannery_web/live/tag_live/form_component.ex:91
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Save"
 | 
					msgid "Save"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:16
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:16
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Send instructions to reset password"
 | 
					msgid "Send instructions to reset password"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/container_live/show.html.heex:80
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/container_live/show.html.heex:53
 | 
					 | 
				
			||||||
msgid "Why not add one?"
 | 
					msgid "Why not add one?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:53
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:52
 | 
					 | 
				
			||||||
msgid "Add"
 | 
					msgid "Add"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/range_live/index.html.heex:17
 | 
					#: lib/cannery_web/live/range_live/index.html.heex:17
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Stage ammo"
 | 
					msgid "Stage ammo"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/range_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/range_live/index.html.heex:12
 | 
					 | 
				
			||||||
msgid "Why not get some ready to shoot?"
 | 
					msgid "Why not get some ready to shoot?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:100
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:101
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/range_live/index.html.heex:39
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.ex:134
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:86
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/range_live/index.html.heex:36
 | 
					 | 
				
			||||||
msgid "Record shots"
 | 
					msgid "Record shots"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:90
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:50
 | 
					 | 
				
			||||||
msgid "Ammo Details"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
#: lib/cannery_web/components/move_ammo_group_component.ex:89
 | 
					 | 
				
			||||||
msgid "Add another container!"
 | 
					msgid "Add another container!"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:94
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:80
 | 
					 | 
				
			||||||
msgid "Move containers"
 | 
					msgid "Move containers"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:126
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/components/move_ammo_group_component.ex:127
 | 
					 | 
				
			||||||
msgid "Select"
 | 
					msgid "Select"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:31
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/index.html.heex:33
 | 
					 | 
				
			||||||
msgid "Copy to clipboard"
 | 
					msgid "Copy to clipboard"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:21
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:18
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:36
 | 
					 | 
				
			||||||
msgid "add a container first"
 | 
					msgid "add a container first"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:74
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:66
 | 
					 | 
				
			||||||
msgid "Create"
 | 
					msgid "Create"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:113
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Change Language"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:139
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Change language"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:60
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "View in Catalog"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:31
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "add an ammo type first"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:60
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Disable"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:64
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Enable"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:80
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Move ammo"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:80
 | 
				
			||||||
 | 
					#, 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:32
 | 
				
			||||||
 | 
					#, 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:31
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Unstage from range"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:153
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Export Data as JSON"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,8 @@ msgstr ""
 | 
				
			|||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2022-04-19 19:32+0000\n"
 | 
					"POT-Creation-Date: 2022-04-19 19:32+0000\n"
 | 
				
			||||||
"PO-Revision-Date: 2022-04-19 21:32+0000\n"
 | 
					"PO-Revision-Date: 2022-05-12 20:28+0000\n"
 | 
				
			||||||
"Last-Translator: shibao <shibao@bubbletea.dev>\n"
 | 
					"Last-Translator: Kaia Estra <kaia@fedora.email>\n"
 | 
				
			||||||
"Language-Team: German <https://weblate.bubbletea.dev/projects/cannery/"
 | 
					"Language-Team: German <https://weblate.bubbletea.dev/projects/cannery/"
 | 
				
			||||||
"actions/de/>\n"
 | 
					"actions/de/>\n"
 | 
				
			||||||
"Language: de\n"
 | 
					"Language: de\n"
 | 
				
			||||||
@@ -12,7 +12,7 @@ msgstr ""
 | 
				
			|||||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
					"Content-Type: text/plain; charset=UTF-8\n"
 | 
				
			||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					"Content-Transfer-Encoding: 8bit\n"
 | 
				
			||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
 | 
					"Plural-Forms: nplurals=2; plural=n != 1;\n"
 | 
				
			||||||
"X-Generator: Weblate 4.11.2\n"
 | 
					"X-Generator: Weblate 4.12.1\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## This file is a PO Template file.
 | 
					## This file is a PO Template file.
 | 
				
			||||||
##
 | 
					##
 | 
				
			||||||
@@ -23,34 +23,36 @@ msgstr ""
 | 
				
			|||||||
## Run "mix gettext.extract" to bring this file up to
 | 
					## Run "mix gettext.extract" to bring this file up to
 | 
				
			||||||
## date. Leave "msgstr"s empty as changing them here has no
 | 
					## date. Leave "msgstr"s empty as changing them here has no
 | 
				
			||||||
## effect: edit them in PO (.po) files instead.
 | 
					## effect: edit them in PO (.po) files instead.
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.ex:44
 | 
					#: 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:40
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add Ammo"
 | 
					msgid "Add Ammo"
 | 
				
			||||||
msgstr "Munition hinzufügen"
 | 
					msgstr "Munition hinzufügen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:24
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:36
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add your first box!"
 | 
					msgid "Add your first box!"
 | 
				
			||||||
msgstr "Fügen Sie ihre erste Box hinzu!"
 | 
					msgstr "Fügen Sie ihre erste Box hinzu!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/container_live/index.html.heex:12
 | 
					#: lib/cannery_web/live/container_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add your first container!"
 | 
					msgid "Add your first container!"
 | 
				
			||||||
msgstr "Fügen Sie ihren ersten Behälter hinzu!"
 | 
					msgstr "Fügen Sie ihren ersten Behälter hinzu!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:12
 | 
					#: lib/cannery_web/live/ammo_type_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add your first type!"
 | 
					msgid "Add your first type!"
 | 
				
			||||||
msgstr "Fügen Sie ihre erste Munitionsart hinzu!"
 | 
					msgstr "Fügen Sie ihre erste Munitionsart hinzu!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:16
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:15
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:45
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:44
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Change email"
 | 
					msgid "Change email"
 | 
				
			||||||
msgstr "Mailadresse ändern"
 | 
					msgstr "Mailadresse ändern"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:60
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:58
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:101
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:99
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Change password"
 | 
					msgid "Change password"
 | 
				
			||||||
msgstr "Passwort ändern"
 | 
					msgstr "Passwort ändern"
 | 
				
			||||||
@@ -60,26 +62,26 @@ msgstr "Passwort ändern"
 | 
				
			|||||||
msgid "Create Invite"
 | 
					msgid "Create Invite"
 | 
				
			||||||
msgstr "Einladung erstellen"
 | 
					msgstr "Einladung erstellen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/templates/user_settings/edit.html.heex:108
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:162
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Delete User"
 | 
					msgid "Delete User"
 | 
				
			||||||
msgstr "Benutzer löschen"
 | 
					msgstr "Benutzer löschen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:43
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:52
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:45
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:45
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Forgot your password?"
 | 
					msgid "Forgot your password?"
 | 
				
			||||||
msgstr "Passwort vergessen?"
 | 
					msgstr "Passwort vergessen?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/index.html.heex:12
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Invite someone new!"
 | 
					msgid "Invite someone new!"
 | 
				
			||||||
msgstr "Laden Sie jemanden ein!"
 | 
					msgstr "Laden Sie jemanden ein!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/components/topbar.ex:108
 | 
					#: lib/cannery_web/components/topbar.ex:137
 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:30
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:30
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:39
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:49
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:48
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:48
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:30
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:30
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:3
 | 
				
			||||||
@@ -88,16 +90,11 @@ msgstr "Laden Sie jemanden ein!"
 | 
				
			|||||||
msgid "Log in"
 | 
					msgid "Log in"
 | 
				
			||||||
msgstr "Einloggen"
 | 
					msgstr "Einloggen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/tag_live/index.html.heex:14
 | 
					#: lib/cannery_web/live/tag_live/index.html.heex:15
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Make your first tag!"
 | 
					msgid "Make your first tag!"
 | 
				
			||||||
msgstr "Erstellen Sie ihren ersten Tag!"
 | 
					msgstr "Erstellen Sie ihren ersten Tag!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:42
 | 
					 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
msgid "New Ammo group"
 | 
					 | 
				
			||||||
msgstr "Neue Munitionsgruppe"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/index.html.heex:17
 | 
					#: lib/cannery_web/live/ammo_type_live/index.html.heex:17
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "New Ammo type"
 | 
					msgid "New Ammo type"
 | 
				
			||||||
@@ -113,13 +110,13 @@ msgstr "Neuer Behälter"
 | 
				
			|||||||
msgid "New Tag"
 | 
					msgid "New Tag"
 | 
				
			||||||
msgstr "Neuer Tag"
 | 
					msgstr "Neuer Tag"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/components/topbar.ex:101
 | 
					#: lib/cannery_web/components/topbar.ex:128
 | 
				
			||||||
#: lib/cannery_web/templates/user_confirmation/new.html.heex:25
 | 
					#: lib/cannery_web/templates/user_confirmation/new.html.heex:26
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:3
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:3
 | 
				
			||||||
#: lib/cannery_web/templates/user_registration/new.html.heex:33
 | 
					#: lib/cannery_web/templates/user_registration/new.html.heex:42
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/edit.html.heex:43
 | 
					#: lib/cannery_web/templates/user_reset_password/edit.html.heex:44
 | 
				
			||||||
#: lib/cannery_web/templates/user_reset_password/new.html.heex:25
 | 
					#: lib/cannery_web/templates/user_reset_password/new.html.heex:26
 | 
				
			||||||
#: lib/cannery_web/templates/user_session/new.html.heex:40
 | 
					#: lib/cannery_web/templates/user_session/new.html.heex:41
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Register"
 | 
					msgid "Register"
 | 
				
			||||||
msgstr "Registrieren"
 | 
					msgstr "Registrieren"
 | 
				
			||||||
@@ -136,13 +133,13 @@ msgstr "Bestätigungsmail erneut senden"
 | 
				
			|||||||
msgid "Reset password"
 | 
					msgid "Reset password"
 | 
				
			||||||
msgstr "Passwort zurücksetzen"
 | 
					msgstr "Passwort zurücksetzen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/components/add_shot_group_component.html.heex:46
 | 
					#: lib/cannery_web/components/add_shot_group_component.html.heex:53
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:73
 | 
					#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:81
 | 
				
			||||||
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:156
 | 
					#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:156
 | 
				
			||||||
#: lib/cannery_web/live/container_live/form_component.html.heex:50
 | 
					#: lib/cannery_web/live/container_live/form_component.html.heex:50
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/form_component.html.heex:28
 | 
					#: lib/cannery_web/live/invite_live/form_component.html.heex:31
 | 
				
			||||||
#: lib/cannery_web/live/range_live/form_component.html.heex:40
 | 
					#: lib/cannery_web/live/range_live/form_component.html.heex:40
 | 
				
			||||||
#: lib/cannery_web/live/tag_live/form_component.ex:66
 | 
					#: lib/cannery_web/live/tag_live/form_component.ex:91
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Save"
 | 
					msgid "Save"
 | 
				
			||||||
msgstr "Speichern"
 | 
					msgstr "Speichern"
 | 
				
			||||||
@@ -152,12 +149,12 @@ msgstr "Speichern"
 | 
				
			|||||||
msgid "Send instructions to reset password"
 | 
					msgid "Send instructions to reset password"
 | 
				
			||||||
msgstr "Anleitung zum Passwort zurücksetzen zusenden"
 | 
					msgstr "Anleitung zum Passwort zurücksetzen zusenden"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/container_live/show.html.heex:53
 | 
					#: lib/cannery_web/live/container_live/show.html.heex:80
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Why not add one?"
 | 
					msgid "Why not add one?"
 | 
				
			||||||
msgstr "Warum fügen Sie keine hinzu?"
 | 
					msgstr "Warum fügen Sie keine hinzu?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:52
 | 
					#: lib/cannery_web/live/container_live/edit_tags_component.html.heex:53
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add"
 | 
					msgid "Add"
 | 
				
			||||||
msgstr "Hinzufügen"
 | 
					msgstr "Hinzufügen"
 | 
				
			||||||
@@ -167,50 +164,101 @@ msgstr "Hinzufügen"
 | 
				
			|||||||
msgid "Stage ammo"
 | 
					msgid "Stage ammo"
 | 
				
			||||||
msgstr "Munition markieren"
 | 
					msgstr "Munition markieren"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/range_live/index.html.heex:12
 | 
					#: lib/cannery_web/live/range_live/index.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Why not get some ready to shoot?"
 | 
					msgid "Why not get some ready to shoot?"
 | 
				
			||||||
msgstr "Warum nicht einige für den Schießstand auswählen?"
 | 
					msgstr "Warum nicht einige für den Schießstand auswählen?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.ex:134
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:100
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:86
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:101
 | 
				
			||||||
#: lib/cannery_web/live/range_live/index.html.heex:36
 | 
					#: lib/cannery_web/live/range_live/index.html.heex:39
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Record shots"
 | 
					msgid "Record shots"
 | 
				
			||||||
msgstr "Schüsse dokumentieren"
 | 
					msgstr "Schüsse dokumentieren"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:50
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:90
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					 | 
				
			||||||
msgid "Ammo Details"
 | 
					 | 
				
			||||||
msgstr "Munitionsdetails"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: lib/cannery_web/components/move_ammo_group_component.ex:89
 | 
					 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Add another container!"
 | 
					msgid "Add another container!"
 | 
				
			||||||
msgstr "Einen weiteren Behälter hinzufügen!"
 | 
					msgstr "Einen weiteren Behälter hinzufügen!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/show.html.heex:80
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:94
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Move containers"
 | 
					msgid "Move containers"
 | 
				
			||||||
msgstr "Behälter verschieben"
 | 
					msgstr "Behälter verschieben"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/components/move_ammo_group_component.ex:127
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:126
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Select"
 | 
					msgid "Select"
 | 
				
			||||||
msgstr "Markieren"
 | 
					msgstr "Markieren"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/invite_live/index.html.heex:33
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:31
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Copy to clipboard"
 | 
					msgid "Copy to clipboard"
 | 
				
			||||||
msgstr "In die Zwischenablage kopieren"
 | 
					msgstr "In die Zwischenablage kopieren"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:18
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:21
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/index.html.heex:36
 | 
					 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "add a container first"
 | 
					msgid "add a container first"
 | 
				
			||||||
msgstr "Zuerst einen Behälter hinzufügen"
 | 
					msgstr "Zuerst einen Behälter hinzufügen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:66
 | 
					#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:74
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "Create"
 | 
					msgid "Create"
 | 
				
			||||||
msgstr "Erstellen"
 | 
					msgstr "Erstellen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:113
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Change Language"
 | 
				
			||||||
 | 
					msgstr "Sprache wechseln"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:139
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Change language"
 | 
				
			||||||
 | 
					msgstr "Sprache wechseln"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/show.html.heex:60
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "View in Catalog"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/ammo_group_live/index.html.heex:31
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "add an ammo type first"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:60
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Disable"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:64
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Enable"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/components/move_ammo_group_component.ex:80
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Move ammo"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/live/invite_live/index.html.heex:80
 | 
				
			||||||
 | 
					#, 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:32
 | 
				
			||||||
 | 
					#, 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:31
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Unstage from range"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: lib/cannery_web/templates/user_settings/edit.html.heex:153
 | 
				
			||||||
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
 | 
					msgid "Export Data as JSON"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -113,7 +113,7 @@ msgstr ""
 | 
				
			|||||||
"Diese Nachricht wurde von %{name} unter %{url} gesandt, einem selbst-"
 | 
					"Diese Nachricht wurde von %{name} unter %{url} gesandt, einem selbst-"
 | 
				
			||||||
"gehosteten Schusswaffenmanager."
 | 
					"gehosteten Schusswaffenmanager."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: lib/cannery_web/templates/layout/email.html.heex:17
 | 
					#: lib/cannery_web/templates/layout/email.html.heex:13
 | 
				
			||||||
#, elixir-autogen, elixir-format
 | 
					#, elixir-autogen, elixir-format
 | 
				
			||||||
msgid "This email was sent from %{name}, the self-hosted firearm tracker website."
 | 
					msgid "This email was sent from %{name}, the self-hosted firearm tracker website."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user