Compare commits
	
		
			270 Commits
		
	
	
		
			0.1.0
			...
			0b27c8f80b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0b27c8f80b | |||
| 0ad1ee47de | |||
| 8ea2b06487 | |||
| 1e4accec9d | |||
| 076d5eea18 | |||
| 5b6bd00047 | |||
| 8dd471afa8 | |||
| 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 | |||
| 4791a2849e | |||
| 7edc9642d4 | |||
| 2ae0c3133c | |||
| 
						 | 
					f284773f68 | ||
| 
						 | 
					d35a81ef39 | ||
| 
						 | 
					4c3ebaec53 | ||
| 
						 | 
					a13a57c6ee | ||
| 
						 | 
					8c7b9f9a9b | ||
| 
						 | 
					7b7604f48f | ||
| 
						 | 
					b1b52b9edc | ||
| 
						 | 
					3fcea8e679 | ||
| 
						 | 
					98ec324a2c | ||
| 
						 | 
					0868789811 | ||
| 
						 | 
					87c62f9ef8 | ||
| 
						 | 
					0919ee2a72 | ||
| 
						 | 
					0b910af077 | ||
| 
						 | 
					cb81eb4116 | ||
| 
						 | 
					b1d7cfe591 | ||
| 1431b92e42 | |||
| c555022ea7 | |||
| 0917aff37b | |||
| 7f9e6f9eff | |||
| 9e754fe630 | |||
| 45d905b384 | |||
| 3297130890 | |||
| d27b54386d | |||
| ebf32c49bf | |||
| 
						 | 
					119f2af6bb | ||
| 9db6b2c316 | |||
| 22281486e0 | |||
| 1b7546aede | |||
| 
						 | 
					e153893a5b | ||
| 
						 | 
					2f7c17aad3 | ||
| 664c65d136 | |||
| 
						 | 
					ef76eb002d | ||
| 
						 | 
					5b40ac4137 | ||
| 
						 | 
					ed20cdd858 | ||
| 
						 | 
					1e0ec82f3e | ||
| 
						 | 
					d07ac801aa | ||
| 
						 | 
					420e7c2d71 | ||
| 
						 | 
					40877d1ac0 | ||
| 
						 | 
					1dd30e6a5b | ||
| 
						 | 
					eedaf33e25 | ||
| b0a100cd6c | |||
| 6455e2710d | |||
| 6523b28aa2 | |||
| bad1a23dfe | |||
| e0ddefe1d7 | |||
| 5d6ecba9f7 | |||
| a2d1ff9b89 | |||
| 34288a0070 | |||
| f9b08222e1 | |||
| c0179f48bd | |||
| 8bb4aab49c | |||
| 9f2cc54738 | |||
| 0309e9d714 | |||
| af4af84515 | |||
| ec6946068e | |||
| f120e54c3e | |||
| 3d115c6383 | |||
| 1d4622a285 | |||
| d9e7948bb0 | |||
| da8c788992 | |||
| bd20820361 | |||
| d0ee81093a | |||
| 6080fdbe64 | |||
| f42aaf9099 | |||
| a6aa6f3386 | |||
| 8405513337 | |||
| d0857eccc1 | |||
| 4c9e707181 | |||
| e6a4fbcfb5 | |||
| a6b2c6181e | |||
| d79d0fa179 | |||
| 46387e8d7a | |||
| dccea1b9de | |||
| 5ca21c64fd | |||
| 9773ccc6ff | |||
| 9cd2bc574b | |||
| ee28de1178 | |||
| 91cf9d0eb5 | |||
| 3ce8eda712 | |||
| 1a78e88b34 | |||
| 968abd04ee | |||
| dc209fa192 | |||
| a80df49fdd | |||
| 92d1d21d00 | |||
| 917f627933 | |||
| 4946a6b119 | |||
| 9f784c3190 | |||
| aa08e212ee | |||
| 80ad939aab | |||
| 08c9cddc78 | |||
| e6285c282b | |||
| 61829fc042 | |||
| e2f8fd3ee5 | |||
| 763c9e521e | |||
| 98747be7c2 | |||
| 91288a9ffa | |||
| a19ec682e6 | |||
| dba53106fb | |||
| 91ff0c14e4 | |||
| bf27511caa | |||
| 4ff2f64a22 | |||
| 146c8e7ab3 | 
							
								
								
									
										14
									
								
								.credo.exs
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.credo.exs
									
									
									
									
									
								
							@@ -157,17 +157,17 @@
 | 
				
			|||||||
        #
 | 
					        #
 | 
				
			||||||
        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
 | 
					        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
 | 
				
			||||||
        #
 | 
					        #
 | 
				
			||||||
        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
 | 
					        {Credo.Check.Consistency.MultiAliasImportRequireUse, []},
 | 
				
			||||||
        {Credo.Check.Consistency.UnusedVariableNames, false},
 | 
					        {Credo.Check.Consistency.UnusedVariableNames, [force: :meaningful]},
 | 
				
			||||||
        {Credo.Check.Design.DuplicatedCode, false},
 | 
					        {Credo.Check.Design.DuplicatedCode, false},
 | 
				
			||||||
        {Credo.Check.Readability.AliasAs, false},
 | 
					        {Credo.Check.Readability.AliasAs, false},
 | 
				
			||||||
        {Credo.Check.Readability.BlockPipe, false},
 | 
					        {Credo.Check.Readability.BlockPipe, false},
 | 
				
			||||||
        {Credo.Check.Readability.ImplTrue, false},
 | 
					        {Credo.Check.Readability.ImplTrue, false},
 | 
				
			||||||
        {Credo.Check.Readability.MultiAlias, false},
 | 
					        {Credo.Check.Readability.MultiAlias, false},
 | 
				
			||||||
        {Credo.Check.Readability.SeparateAliasRequire, false},
 | 
					        {Credo.Check.Readability.SeparateAliasRequire, []},
 | 
				
			||||||
        {Credo.Check.Readability.SinglePipe, false},
 | 
					        {Credo.Check.Readability.SinglePipe, false},
 | 
				
			||||||
        {Credo.Check.Readability.Specs, false},
 | 
					        {Credo.Check.Readability.Specs, false},
 | 
				
			||||||
        {Credo.Check.Readability.StrictModuleLayout, false},
 | 
					        {Credo.Check.Readability.StrictModuleLayout, []},
 | 
				
			||||||
        {Credo.Check.Readability.WithCustomTaggedTuple, false},
 | 
					        {Credo.Check.Readability.WithCustomTaggedTuple, false},
 | 
				
			||||||
        {Credo.Check.Refactor.ABCSize, false},
 | 
					        {Credo.Check.Refactor.ABCSize, false},
 | 
				
			||||||
        {Credo.Check.Refactor.AppendSingleItem, false},
 | 
					        {Credo.Check.Refactor.AppendSingleItem, false},
 | 
				
			||||||
@@ -176,9 +176,9 @@
 | 
				
			|||||||
        {Credo.Check.Refactor.NegatedIsNil, false},
 | 
					        {Credo.Check.Refactor.NegatedIsNil, false},
 | 
				
			||||||
        {Credo.Check.Refactor.PipeChainStart, false},
 | 
					        {Credo.Check.Refactor.PipeChainStart, false},
 | 
				
			||||||
        {Credo.Check.Refactor.VariableRebinding, false},
 | 
					        {Credo.Check.Refactor.VariableRebinding, false},
 | 
				
			||||||
        {Credo.Check.Warning.LeakyEnvironment, false},
 | 
					        {Credo.Check.Warning.LeakyEnvironment, []},
 | 
				
			||||||
        {Credo.Check.Warning.MapGetUnsafePass, false},
 | 
					        {Credo.Check.Warning.MapGetUnsafePass, []},
 | 
				
			||||||
        {Credo.Check.Warning.UnsafeToAtom, false}
 | 
					        {Credo.Check.Warning.UnsafeToAtom, []}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #
 | 
					        #
 | 
				
			||||||
        # Custom checks can be created using `mix credo.gen.check`.
 | 
					        # Custom checks can be created using `mix credo.gen.check`.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -16,30 +16,56 @@ steps:
 | 
				
			|||||||
      - assets/node_modules/
 | 
					      - assets/node_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: test
 | 
					- name: test
 | 
				
			||||||
  image: bitwalker/alpine-elixir-phoenix:1.13
 | 
					  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
 | 
				
			||||||
  commands:
 | 
					  commands:
 | 
				
			||||||
 | 
					  - apk add --no-cache build-base npm git python3
 | 
				
			||||||
  - mix local.rebar --force
 | 
					  - mix local.rebar --force
 | 
				
			||||||
  - mix local.hex --force
 | 
					  - mix local.hex --force
 | 
				
			||||||
  - mix deps.get
 | 
					  - mix deps.get
 | 
				
			||||||
  - npm install --prefix assets
 | 
					  - mix deps.compile
 | 
				
			||||||
  - mix test
 | 
					  - npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
 | 
				
			||||||
 | 
					  - npm run --prefix ./assets deploy
 | 
				
			||||||
 | 
					  - mix do phx.digest, gettext.extract
 | 
				
			||||||
 | 
					  - mix test.all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: build and publish
 | 
					- name: build and publish stable
 | 
				
			||||||
  image: plugins/docker
 | 
					  image: thegeeklab/drone-docker-buildx
 | 
				
			||||||
 | 
					  privileged: true
 | 
				
			||||||
  settings:
 | 
					  settings:
 | 
				
			||||||
    repo: shibaobun/cannery
 | 
					    repo: shibaobun/cannery
 | 
				
			||||||
    tags: latest
 | 
					    purge: true
 | 
				
			||||||
 | 
					    compress: true
 | 
				
			||||||
 | 
					    platforms: linux/amd64,linux/arm/v7
 | 
				
			||||||
    username:
 | 
					    username:
 | 
				
			||||||
      from_secret: docker_username
 | 
					      from_secret: docker_username
 | 
				
			||||||
    password:
 | 
					    password:
 | 
				
			||||||
      from_secret: docker_password
 | 
					      from_secret: docker_password
 | 
				
			||||||
 | 
					    tags: latest
 | 
				
			||||||
  when:
 | 
					  when:
 | 
				
			||||||
    branch:
 | 
					    branch:
 | 
				
			||||||
    - stable
 | 
					    - stable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- name: build and publish tagged version
 | 
				
			||||||
 | 
					  image: thegeeklab/drone-docker-buildx
 | 
				
			||||||
 | 
					  privileged: true
 | 
				
			||||||
 | 
					  settings:
 | 
				
			||||||
 | 
					    repo: shibaobun/cannery
 | 
				
			||||||
 | 
					    purge: true
 | 
				
			||||||
 | 
					    compress: true
 | 
				
			||||||
 | 
					    platforms: linux/amd64,linux/arm/v7
 | 
				
			||||||
 | 
					    username:
 | 
				
			||||||
 | 
					      from_secret: docker_username
 | 
				
			||||||
 | 
					    password:
 | 
				
			||||||
 | 
					      from_secret: docker_password
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - ${DRONE_TAG}
 | 
				
			||||||
 | 
					  when:
 | 
				
			||||||
 | 
					    event:
 | 
				
			||||||
 | 
					    - tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: rebuild-cache
 | 
					- name: rebuild-cache
 | 
				
			||||||
  image: drillster/drone-volume-cache
 | 
					  image: drillster/drone-volume-cache
 | 
				
			||||||
  volumes:
 | 
					  volumes:
 | 
				
			||||||
@@ -63,7 +89,7 @@ services:
 | 
				
			|||||||
volumes:
 | 
					volumes:
 | 
				
			||||||
  - name: cache
 | 
					  - name: cache
 | 
				
			||||||
    host:
 | 
					    host:
 | 
				
			||||||
      path: /tmp/drone-cache
 | 
					      path: /run/media/default/ssdsrv/gitea/drone-cache
 | 
				
			||||||
  - name: docker_sock
 | 
					  - name: docker_sock
 | 
				
			||||||
    host:
 | 
					    host:
 | 
				
			||||||
      path: /var/run/docker.sock
 | 
					      path: /var/run/docker.sock
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,5 +2,5 @@
 | 
				
			|||||||
  import_deps: [:ecto, :phoenix],
 | 
					  import_deps: [:ecto, :phoenix],
 | 
				
			||||||
  inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
 | 
					  inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
 | 
				
			||||||
  subdirectories: ["priv/*/migrations"],
 | 
					  subdirectories: ["priv/*/migrations"],
 | 
				
			||||||
  plugins: [HeexFormatter]
 | 
					  plugins: [Phoenix.LiveView.HTMLFormatter]
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
elixir 1.13.2-otp-24
 | 
					elixir 1.14.1-otp-25
 | 
				
			||||||
erlang 24.2
 | 
					erlang 25.1.2
 | 
				
			||||||
nodejs 16.13.2
 | 
					nodejs 18.12.1
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										138
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					# v0.8.1
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					- Show topbar on form submit/page refresh
 | 
				
			||||||
 | 
					- Make loading/reconnection less intrusive
 | 
				
			||||||
 | 
					- Add QR code for invite link
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 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
 | 
				
			||||||
 | 
					- Add French translation: Thank you [duponin](https://udongein.xyz/users/duponin)!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.5.0
 | 
				
			||||||
 | 
					- Add German translation: Thank you [Kaia](https://shitposter.club/users/kaia)!
 | 
				
			||||||
 | 
					- Fix not being able to edit ammo group when fully used up
 | 
				
			||||||
 | 
					- Fix bug with average price per round calculation
 | 
				
			||||||
 | 
					- Show average price per round on ammo type table
 | 
				
			||||||
 | 
					- Use Elixir v1.13.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.4.1
 | 
				
			||||||
 | 
					- Fix button and tag text wrapping
 | 
				
			||||||
 | 
					- Code quality fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.4.0
 | 
				
			||||||
 | 
					- Make tables sortable
 | 
				
			||||||
 | 
					- Add link to changelog from version number
 | 
				
			||||||
 | 
					- Fix some elements flashing with black background
 | 
				
			||||||
 | 
					- Fix bug with moving ammo group to new container
 | 
				
			||||||
 | 
					- Fix bug with no error showing up for create ammo group form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.3.0
 | 
				
			||||||
 | 
					- Fix ammo type counts not showing when count is 0
 | 
				
			||||||
 | 
					- Add prompt to create first container before first ammo group
 | 
				
			||||||
 | 
					- Edit and delete shot groups from ammo group show page
 | 
				
			||||||
 | 
					- Use today's date when adding new shot groups
 | 
				
			||||||
 | 
					- Create multiple ammo groups at one time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.2.3
 | 
				
			||||||
 | 
					- Fix modals with overflowing forms
 | 
				
			||||||
 | 
					- Fix grids having uneven margins in phone mode
 | 
				
			||||||
 | 
					- Add page titles to registration and setting pages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.2.2
 | 
				
			||||||
 | 
					- Fix loading and reconnecting pages not being fixed
 | 
				
			||||||
 | 
					- Fix closing modal in some cases not triggering a page reload
 | 
				
			||||||
 | 
					- Fix error when display container and tag edit routes from a fresh reload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.2.1
 | 
				
			||||||
 | 
					- Fix checkbox spacing for mobile view
 | 
				
			||||||
 | 
					- Fix spacing with form elements in mobile view
 | 
				
			||||||
 | 
					- Fix user card spacing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.2.0
 | 
				
			||||||
 | 
					- Add or remove tags from containers list and details page
 | 
				
			||||||
 | 
					- Show tags on containers
 | 
				
			||||||
 | 
					- Add "Cannery" to page titles
 | 
				
			||||||
 | 
					- Don't show true/false column for ammo types if all values are false
 | 
				
			||||||
 | 
					- Fix ammo type firing type display
 | 
				
			||||||
 | 
					- Show original count, current value, and percentage remaining for ammo groups
 | 
				
			||||||
 | 
					- Show shot history for an ammo group
 | 
				
			||||||
 | 
					- Show ammo round totals and total shot for ammo types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.0
 | 
				
			||||||
 | 
					- Initial release!
 | 
				
			||||||
@@ -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,8 @@ 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`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## `MIX_ENV=test`
 | 
					## `MIX_ENV=test`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -141,3 +141,7 @@ In `prod` mode (or in the Docker container), Cannery will listen for the same en
 | 
				
			|||||||
Thank you so much for your contributions!
 | 
					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)
 | 
				
			||||||
 | 
					- 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-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,5 +1,7 @@
 | 
				
			|||||||
# Cannery
 | 
					# Cannery
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The self-hosted firearm tracker website.
 | 
					The self-hosted firearm tracker website.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Easy to Use: Cannery lets you easily keep an eye on your ammo levels before
 | 
					* Easy to Use: Cannery lets you easily keep an eye on your ammo levels before
 | 
				
			||||||
@@ -61,7 +63,8 @@ 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`
 | 
				
			||||||
- `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`.
 | 
				
			||||||
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
 | 
					- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,8 +17,7 @@
 | 
				
			|||||||
    -o-transform: scale(1.5);
 | 
					    -o-transform: scale(1.5);
 | 
				
			||||||
    transform: scale(1.5);
 | 
					    transform: scale(1.5);
 | 
				
			||||||
    padding: 10px;
 | 
					    padding: 10px;
 | 
				
			||||||
    margin-left: auto;
 | 
					    margin: 1em auto;
 | 
				
			||||||
    margin-right: auto;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .title {
 | 
					  .title {
 | 
				
			||||||
@@ -26,6 +25,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .btn {
 | 
					  .btn {
 | 
				
			||||||
 | 
					    @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,6 +52,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .link {
 | 
					  .link {
 | 
				
			||||||
 | 
					    @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;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,8 +24,9 @@ import 'phoenix_html'
 | 
				
			|||||||
// Establish Phoenix Socket and LiveView configuration.
 | 
					// Establish Phoenix Socket and LiveView configuration.
 | 
				
			||||||
import { Socket } from 'phoenix'
 | 
					import { Socket } from 'phoenix'
 | 
				
			||||||
import { LiveSocket } from 'phoenix_live_view'
 | 
					import { LiveSocket } from 'phoenix_live_view'
 | 
				
			||||||
import topbar from '../vendor/topbar'
 | 
					import topbar from '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
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,10 @@
 | 
				
			|||||||
// update. https://github.com/phoenixframework/phoenix_live_view/issues/1011
 | 
					// update. https://github.com/phoenixframework/phoenix_live_view/issues/1011
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  attrs () { return this.el.getAttribute('data-attrs').split(', ') },
 | 
					  attrs () {
 | 
				
			||||||
 | 
					    const attrs = this.el.getAttribute('data-attrs')
 | 
				
			||||||
 | 
					    if (attrs) { return attrs.split(', ') } else { return [] }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  beforeUpdate () { this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
 | 
					  beforeUpdate () { this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
 | 
				
			||||||
  updated () { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
 | 
					  updated () { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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) }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15628
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15628
									
								
								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
 | 
				
			||||||
@@ -28,13 +22,21 @@ module.exports = {
 | 
				
			|||||||
        128: '32rem',
 | 
					        128: '32rem',
 | 
				
			||||||
        192: '48rem',
 | 
					        192: '48rem',
 | 
				
			||||||
        256: '64rem'
 | 
					        256: '64rem'
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
  variants: {
 | 
					      minWidth: {
 | 
				
			||||||
    extend: {
 | 
					        4: '1rem',
 | 
				
			||||||
      backgroundColor: ['active'],
 | 
					        8: '2rem',
 | 
				
			||||||
      borderColor: ['active']
 | 
					        12: '3rem',
 | 
				
			||||||
 | 
					        16: '4rem',
 | 
				
			||||||
 | 
					        20: '8rem'
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      maxWidth: {
 | 
				
			||||||
 | 
					        4: '1rem',
 | 
				
			||||||
 | 
					        8: '2rem',
 | 
				
			||||||
 | 
					        12: '3rem',
 | 
				
			||||||
 | 
					        16: '4rem',
 | 
				
			||||||
 | 
					        20: '8rem'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  plugins: []
 | 
					  plugins: []
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										157
									
								
								assets/vendor/topbar.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										157
									
								
								assets/vendor/topbar.js
									
									
									
									
										vendored
									
									
								
							@@ -1,157 +0,0 @@
 | 
				
			|||||||
/**
 | 
					 | 
				
			||||||
 * @license MIT
 | 
					 | 
				
			||||||
 * topbar 1.0.0, 2021-01-06
 | 
					 | 
				
			||||||
 * https://buunguyen.github.io/topbar
 | 
					 | 
				
			||||||
 * Copyright (c) 2021 Buu Nguyen
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
(function (window, document) {
 | 
					 | 
				
			||||||
  "use strict";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // https://gist.github.com/paulirish/1579671
 | 
					 | 
				
			||||||
  (function () {
 | 
					 | 
				
			||||||
    var lastTime = 0;
 | 
					 | 
				
			||||||
    var vendors = ["ms", "moz", "webkit", "o"];
 | 
					 | 
				
			||||||
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
 | 
					 | 
				
			||||||
      window.requestAnimationFrame =
 | 
					 | 
				
			||||||
        window[vendors[x] + "RequestAnimationFrame"];
 | 
					 | 
				
			||||||
      window.cancelAnimationFrame =
 | 
					 | 
				
			||||||
        window[vendors[x] + "CancelAnimationFrame"] ||
 | 
					 | 
				
			||||||
        window[vendors[x] + "CancelRequestAnimationFrame"];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!window.requestAnimationFrame)
 | 
					 | 
				
			||||||
      window.requestAnimationFrame = function (callback, element) {
 | 
					 | 
				
			||||||
        var currTime = new Date().getTime();
 | 
					 | 
				
			||||||
        var timeToCall = Math.max(0, 16 - (currTime - lastTime));
 | 
					 | 
				
			||||||
        var id = window.setTimeout(function () {
 | 
					 | 
				
			||||||
          callback(currTime + timeToCall);
 | 
					 | 
				
			||||||
        }, timeToCall);
 | 
					 | 
				
			||||||
        lastTime = currTime + timeToCall;
 | 
					 | 
				
			||||||
        return id;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    if (!window.cancelAnimationFrame)
 | 
					 | 
				
			||||||
      window.cancelAnimationFrame = function (id) {
 | 
					 | 
				
			||||||
        clearTimeout(id);
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
  })();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  var canvas,
 | 
					 | 
				
			||||||
    progressTimerId,
 | 
					 | 
				
			||||||
    fadeTimerId,
 | 
					 | 
				
			||||||
    currentProgress,
 | 
					 | 
				
			||||||
    showing,
 | 
					 | 
				
			||||||
    addEvent = function (elem, type, handler) {
 | 
					 | 
				
			||||||
      if (elem.addEventListener) elem.addEventListener(type, handler, false);
 | 
					 | 
				
			||||||
      else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
 | 
					 | 
				
			||||||
      else elem["on" + type] = handler;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    options = {
 | 
					 | 
				
			||||||
      autoRun: true,
 | 
					 | 
				
			||||||
      barThickness: 3,
 | 
					 | 
				
			||||||
      barColors: {
 | 
					 | 
				
			||||||
        0: "rgba(26,  188, 156, .9)",
 | 
					 | 
				
			||||||
        ".25": "rgba(52,  152, 219, .9)",
 | 
					 | 
				
			||||||
        ".50": "rgba(241, 196, 15,  .9)",
 | 
					 | 
				
			||||||
        ".75": "rgba(230, 126, 34,  .9)",
 | 
					 | 
				
			||||||
        "1.0": "rgba(211, 84,  0,   .9)",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      shadowBlur: 10,
 | 
					 | 
				
			||||||
      shadowColor: "rgba(0,   0,   0,   .6)",
 | 
					 | 
				
			||||||
      className: null,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    repaint = function () {
 | 
					 | 
				
			||||||
      canvas.width = window.innerWidth;
 | 
					 | 
				
			||||||
      canvas.height = options.barThickness * 5; // need space for shadow
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var ctx = canvas.getContext("2d");
 | 
					 | 
				
			||||||
      ctx.shadowBlur = options.shadowBlur;
 | 
					 | 
				
			||||||
      ctx.shadowColor = options.shadowColor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
 | 
					 | 
				
			||||||
      for (var stop in options.barColors)
 | 
					 | 
				
			||||||
        lineGradient.addColorStop(stop, options.barColors[stop]);
 | 
					 | 
				
			||||||
      ctx.lineWidth = options.barThickness;
 | 
					 | 
				
			||||||
      ctx.beginPath();
 | 
					 | 
				
			||||||
      ctx.moveTo(0, options.barThickness / 2);
 | 
					 | 
				
			||||||
      ctx.lineTo(
 | 
					 | 
				
			||||||
        Math.ceil(currentProgress * canvas.width),
 | 
					 | 
				
			||||||
        options.barThickness / 2
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      ctx.strokeStyle = lineGradient;
 | 
					 | 
				
			||||||
      ctx.stroke();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    createCanvas = function () {
 | 
					 | 
				
			||||||
      canvas = document.createElement("canvas");
 | 
					 | 
				
			||||||
      var style = canvas.style;
 | 
					 | 
				
			||||||
      style.position = "fixed";
 | 
					 | 
				
			||||||
      style.top = style.left = style.right = style.margin = style.padding = 0;
 | 
					 | 
				
			||||||
      style.zIndex = 100001;
 | 
					 | 
				
			||||||
      style.display = "none";
 | 
					 | 
				
			||||||
      if (options.className) canvas.classList.add(options.className);
 | 
					 | 
				
			||||||
      document.body.appendChild(canvas);
 | 
					 | 
				
			||||||
      addEvent(window, "resize", repaint);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    topbar = {
 | 
					 | 
				
			||||||
      config: function (opts) {
 | 
					 | 
				
			||||||
        for (var key in opts)
 | 
					 | 
				
			||||||
          if (options.hasOwnProperty(key)) options[key] = opts[key];
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      show: function () {
 | 
					 | 
				
			||||||
        if (showing) return;
 | 
					 | 
				
			||||||
        showing = true;
 | 
					 | 
				
			||||||
        if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
 | 
					 | 
				
			||||||
        if (!canvas) createCanvas();
 | 
					 | 
				
			||||||
        canvas.style.opacity = 1;
 | 
					 | 
				
			||||||
        canvas.style.display = "block";
 | 
					 | 
				
			||||||
        topbar.progress(0);
 | 
					 | 
				
			||||||
        if (options.autoRun) {
 | 
					 | 
				
			||||||
          (function loop() {
 | 
					 | 
				
			||||||
            progressTimerId = window.requestAnimationFrame(loop);
 | 
					 | 
				
			||||||
            topbar.progress(
 | 
					 | 
				
			||||||
              "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          })();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      progress: function (to) {
 | 
					 | 
				
			||||||
        if (typeof to === "undefined") return currentProgress;
 | 
					 | 
				
			||||||
        if (typeof to === "string") {
 | 
					 | 
				
			||||||
          to =
 | 
					 | 
				
			||||||
            (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
 | 
					 | 
				
			||||||
              ? currentProgress
 | 
					 | 
				
			||||||
              : 0) + parseFloat(to);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        currentProgress = to > 1 ? 1 : to;
 | 
					 | 
				
			||||||
        repaint();
 | 
					 | 
				
			||||||
        return currentProgress;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      hide: function () {
 | 
					 | 
				
			||||||
        if (!showing) return;
 | 
					 | 
				
			||||||
        showing = false;
 | 
					 | 
				
			||||||
        if (progressTimerId != null) {
 | 
					 | 
				
			||||||
          window.cancelAnimationFrame(progressTimerId);
 | 
					 | 
				
			||||||
          progressTimerId = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        (function loop() {
 | 
					 | 
				
			||||||
          if (topbar.progress("+.1") >= 1) {
 | 
					 | 
				
			||||||
            canvas.style.opacity -= 0.05;
 | 
					 | 
				
			||||||
            if (canvas.style.opacity <= 0.05) {
 | 
					 | 
				
			||||||
              canvas.style.display = "none";
 | 
					 | 
				
			||||||
              fadeTimerId = null;
 | 
					 | 
				
			||||||
              return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          fadeTimerId = window.requestAnimationFrame(loop);
 | 
					 | 
				
			||||||
        })();
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (typeof module === "object" && typeof module.exports === "object") {
 | 
					 | 
				
			||||||
    module.exports = topbar;
 | 
					 | 
				
			||||||
  } else if (typeof define === "function" && define.amd) {
 | 
					 | 
				
			||||||
    define(function () {
 | 
					 | 
				
			||||||
      return topbar;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    this.topbar = topbar;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}.call(this, window, document));
 | 
					 | 
				
			||||||
@@ -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: '../' }]
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,18 +14,19 @@ end
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
config :cannery, CanneryWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
 | 
					config :cannery, CanneryWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set locale
 | 
					# Set default locale
 | 
				
			||||||
Gettext.put_locale(System.get_env("LOCALE") || "en_US")
 | 
					config :gettext, :default_locale, System.get_env("LOCALE", "en_US")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: []
 | 
					maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
database_url =
 | 
					database_url =
 | 
				
			||||||
  if config_env() == :test do
 | 
					  if config_env() == :test do
 | 
				
			||||||
    System.get_env("TEST_DATABASE_URL") ||
 | 
					    System.get_env(
 | 
				
			||||||
 | 
					      "TEST_DATABASE_URL",
 | 
				
			||||||
      "ecto://postgres:postgres@localhost/cannery_test#{System.get_env("MIX_TEST_PARTITION")}"
 | 
					      "ecto://postgres:postgres@localhost/cannery_test#{System.get_env("MIX_TEST_PARTITION")}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  else
 | 
					  else
 | 
				
			||||||
    System.get_env("DATABASE_URL") ||
 | 
					    System.get_env("DATABASE_URL", "ecto://postgres:postgres@cannery-db/cannery")
 | 
				
			||||||
      "ecto://postgres:postgres@cannery-db/cannery"
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
host =
 | 
					host =
 | 
				
			||||||
@@ -40,7 +41,7 @@ interface =
 | 
				
			|||||||
config :cannery, Cannery.Repo,
 | 
					config :cannery, Cannery.Repo,
 | 
				
			||||||
  # ssl: true,
 | 
					  # ssl: true,
 | 
				
			||||||
  url: database_url,
 | 
					  url: database_url,
 | 
				
			||||||
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
 | 
					  pool_size: String.to_integer(System.get_env("POOL_SIZE", "10")),
 | 
				
			||||||
  socket_options: maybe_ipv6
 | 
					  socket_options: maybe_ipv6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
config :cannery, CanneryWeb.Endpoint,
 | 
					config :cannery, CanneryWeb.Endpoint,
 | 
				
			||||||
@@ -49,10 +50,10 @@ config :cannery, CanneryWeb.Endpoint,
 | 
				
			|||||||
    # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
 | 
					    # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
 | 
				
			||||||
    # for details about using IPv6 vs IPv4 and loopback vs public addresses.
 | 
					    # for details about using IPv6 vs IPv4 and loopback vs public addresses.
 | 
				
			||||||
    ip: interface,
 | 
					    ip: interface,
 | 
				
			||||||
    port: String.to_integer(System.get_env("PORT") || "4000")
 | 
					    port: String.to_integer(System.get_env("PORT", "4000"))
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  server: true,
 | 
					  server: true,
 | 
				
			||||||
  registration: System.get_env("REGISTRATION") || "invite"
 | 
					  registration: System.get_env("REGISTRATION", "invite")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if config_env() == :prod do
 | 
					if config_env() == :prod do
 | 
				
			||||||
  # The secret key base is used to sign/encrypt cookies and other secrets.
 | 
					  # The secret key base is used to sign/encrypt cookies and other secrets.
 | 
				
			||||||
@@ -76,12 +77,12 @@ if config_env() == :prod do
 | 
				
			|||||||
  config :cannery, Cannery.Mailer,
 | 
					  config :cannery, Cannery.Mailer,
 | 
				
			||||||
    adapter: Swoosh.Adapters.SMTP,
 | 
					    adapter: Swoosh.Adapters.SMTP,
 | 
				
			||||||
    relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"),
 | 
					    relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"),
 | 
				
			||||||
    port: System.get_env("SMTP_PORT") || 587,
 | 
					    port: System.get_env("SMTP_PORT", 587),
 | 
				
			||||||
    username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"),
 | 
					    username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"),
 | 
				
			||||||
    password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
 | 
					    password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
 | 
				
			||||||
    ssl: System.get_env("SMTP_SSL") == "true",
 | 
					    ssl: System.get_env("SMTP_SSL") == "true",
 | 
				
			||||||
    email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}",
 | 
					    email_from: System.get_env("EMAIL_FROM", "no-reply@#{System.get_env("HOST")}"),
 | 
				
			||||||
    email_name: System.get_env("EMAIL_NAME") || "Cannery"
 | 
					    email_name: System.get_env("EMAIL_NAME", "Cannery")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # ## Using releases
 | 
					  # ## Using releases
 | 
				
			||||||
  #
 | 
					  #
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 | 
					    Multi.new()
 | 
				
			||||||
 | 
					    |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
 | 
				
			||||||
 | 
					    |> Multi.insert(:add_user, fn %{users_count: count} ->
 | 
				
			||||||
      # if no registered users, make first user an admin
 | 
					      # if no registered users, make first user an admin
 | 
				
			||||||
    role =
 | 
					      role = if count == 0, do: "admin", else: "user"
 | 
				
			||||||
      if Repo.one!(from u in User, select: count(u.id), distinct: true) == 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}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -196,11 +201,11 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
         {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
 | 
					         {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
 | 
				
			||||||
      :ok
 | 
					      :ok
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> :error
 | 
					      _error_tuple -> :error
 | 
				
			||||||
    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
 | 
				
			||||||
@@ -265,10 +271,39 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
					      {:ok, %{user: user}} -> {:ok, user}
 | 
				
			||||||
      {:error, :user, changeset, _} -> {:error, changeset}
 | 
					      {:error, :user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
    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,14 +400,14 @@ 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),
 | 
				
			||||||
         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
 | 
					         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
 | 
				
			||||||
      {:ok, user}
 | 
					      {:ok, user}
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> :error
 | 
					      _error_tuple -> :error
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -414,13 +449,13 @@ 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
 | 
				
			||||||
      user
 | 
					      user
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> nil
 | 
					      _error_tuple -> nil
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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))
 | 
				
			||||||
@@ -444,7 +480,7 @@ defmodule Cannery.Accounts do
 | 
				
			|||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
					      {:ok, %{user: user}} -> {:ok, user}
 | 
				
			||||||
      {:error, :user, changeset, _} -> {:error, changeset}
 | 
					      {:error, :user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
@@ -171,7 +180,7 @@ defmodule Cannery.Accounts.User do
 | 
				
			|||||||
    Bcrypt.verify_pass(password, hashed_password)
 | 
					    Bcrypt.verify_pass(password, hashed_password)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def valid_password?(_, _) do
 | 
					  def valid_password?(_invalid_user, _invalid_password) do
 | 
				
			||||||
    Bcrypt.no_user_verify()
 | 
					    Bcrypt.no_user_verify()
 | 
				
			||||||
    false
 | 
					    false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -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,26 +98,25 @@ 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,
 | 
					 | 
				
			||||||
        %User{id: user_id},
 | 
					 | 
				
			||||||
        %AmmoGroup{id: ammo_group_id, count: ammo_group_count, user_id: user_id} = ammo_group
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    attrs = attrs |> Map.merge(%{"user_id" => user_id, "ammo_group_id" => ammo_group_id})
 | 
					 | 
				
			||||||
    changeset = %ShotGroup{} |> ShotGroup.create_changeset(attrs)
 | 
					 | 
				
			||||||
    shot_group_count = changeset |> Changeset.get_field(:count)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if shot_group_count > ammo_group_count do
 | 
					 | 
				
			||||||
      error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count)
 | 
					 | 
				
			||||||
      changeset = changeset |> Changeset.add_error(:count, error)
 | 
					 | 
				
			||||||
      {:error, changeset}
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
    Multi.new()
 | 
					    Multi.new()
 | 
				
			||||||
      |> Multi.insert(:create_shot_group, changeset)
 | 
					    |> Multi.insert(
 | 
				
			||||||
 | 
					      :create_shot_group,
 | 
				
			||||||
 | 
					      %ShotGroup{} |> ShotGroup.create_changeset(user, ammo_group, attrs)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> Multi.run(
 | 
				
			||||||
 | 
					      :ammo_group,
 | 
				
			||||||
 | 
					      fn repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} ->
 | 
				
			||||||
 | 
					        {:ok,
 | 
				
			||||||
 | 
					         repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)}
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    |> Multi.update(
 | 
					    |> Multi.update(
 | 
				
			||||||
      :update_ammo_group,
 | 
					      :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})
 | 
					        ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count})
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
@@ -87,7 +125,6 @@ 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
 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Updates a shot_group.
 | 
					  Updates a shot_group.
 | 
				
			||||||
@@ -102,37 +139,34 @@ 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} =
 | 
					 | 
				
			||||||
      ammo_group = ammo_group_id |> Ammo.get_ammo_group!(user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    changeset = shot_group |> ShotGroup.update_changeset(attrs)
 | 
					 | 
				
			||||||
    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 |> Changeset.add_error(:count, error)
 | 
					 | 
				
			||||||
        {:error, changeset}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      new_shot_group_count <= 0 ->
 | 
					 | 
				
			||||||
        error = dgettext("errors", "Count must be at least 1")
 | 
					 | 
				
			||||||
        changeset = changeset |> Changeset.add_error(:count, error)
 | 
					 | 
				
			||||||
        {:error, changeset}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      true ->
 | 
					 | 
				
			||||||
    Multi.new()
 | 
					    Multi.new()
 | 
				
			||||||
        |> Multi.update(:update_shot_group, changeset)
 | 
					    |> Multi.update(
 | 
				
			||||||
 | 
					      :update_shot_group,
 | 
				
			||||||
 | 
					      shot_group |> ShotGroup.update_changeset(user, attrs)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> Multi.run(
 | 
				
			||||||
 | 
					      :ammo_group,
 | 
				
			||||||
 | 
					      fn repo, %{update_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 - shot_diff_to_add})
 | 
					           update_shot_group: %{count: new_count},
 | 
				
			||||||
 | 
					           ammo_group: %{count: ammo_group_count} = ammo_group
 | 
				
			||||||
 | 
					         } ->
 | 
				
			||||||
 | 
					        shot_diff_to_add = new_count - count
 | 
				
			||||||
 | 
					        new_ammo_group_count = ammo_group_count - shot_diff_to_add
 | 
				
			||||||
 | 
					        ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count})
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
@@ -141,7 +175,6 @@ 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
 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes a shot_group.
 | 
					  Deletes a shot_group.
 | 
				
			||||||
@@ -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,11 +3,15 @@ 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.Ammo.{AmmoGroup, AmmoType}
 | 
					  alias Cannery.Ammo.{AmmoGroup, AmmoType}
 | 
				
			||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ammo_group_create_limit 10_000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns the list of ammo_types.
 | 
					  Returns the list of ammo_types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,19 +92,76 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets the average cost of a single ammo type
 | 
					  Gets the average cost of a single ammo type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_average_cost_for_ammo_type!(%AmmoType{id: 123}, %User{id: 123})
 | 
				
			||||||
 | 
					      1.50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_average_cost_for_ammo_type!(AmmoType.t(), User.t()) :: float() | nil
 | 
				
			||||||
 | 
					  def get_average_cost_for_ammo_type!(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id}
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    sg_total_query =
 | 
				
			||||||
 | 
					      from sg in ShotGroup,
 | 
				
			||||||
 | 
					        where: not (sg.count |> is_nil()),
 | 
				
			||||||
 | 
					        group_by: sg.ammo_group_id,
 | 
				
			||||||
 | 
					        select: %{ammo_group_id: sg.ammo_group_id, total: sum(sg.count)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        as: :ammo_group,
 | 
				
			||||||
 | 
					        left_join: sg_query in subquery(sg_total_query),
 | 
				
			||||||
 | 
					        on: ag.id == sg_query.ammo_group_id,
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        where: not (ag.price_paid |> is_nil()),
 | 
				
			||||||
 | 
					        select: sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0))
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Gets the total number of rounds for an ammo type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Raises `Ecto.NoResultsError` if the Ammo type does not exist.
 | 
					  Raises `Ecto.NoResultsError` if the Ammo type does not exist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_ammo_type!(123, %User{id: 123})
 | 
					      iex> get_round_count_for_ammo_type(123, %User{id: 123})
 | 
				
			||||||
      %AmmoType{}
 | 
					      35
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_ammo_type!(456, %User{id: 123})
 | 
					      iex> get_round_count_for_ammo_type(456, %User{id: 123})
 | 
				
			||||||
      ** (Ecto.NoResultsError)
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_average_cost_for_ammo_type!(AmmoType.t(), User.t()) :: float()
 | 
					  @spec get_round_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
 | 
				
			||||||
  def get_average_cost_for_ammo_type!(
 | 
					  def get_round_count_for_ammo_type(
 | 
				
			||||||
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
 | 
					        %User{id: user_id}
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
 | 
					        select: sum(ag.count)
 | 
				
			||||||
 | 
					    ) || 0
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Gets the total number of rounds shot for an ammo type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Raises `Ecto.NoResultsError` if the Ammo type does not exist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_used_count_for_ammo_type(123, %User{id: 123})
 | 
				
			||||||
 | 
					      35
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_used_count_for_ammo_type(456, %User{id: 123})
 | 
				
			||||||
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_used_count_for_ammo_type(
 | 
				
			||||||
        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
					        %AmmoType{id: ammo_type_id, user_id: user_id},
 | 
				
			||||||
        %User{id: user_id}
 | 
					        %User{id: user_id}
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
@@ -62,9 +169,31 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
      from ag in AmmoGroup,
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
        left_join: sg in assoc(ag, :shot_groups),
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
        where: ag.ammo_type_id == ^ammo_type_id,
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
        where: not (ag.price_paid |> is_nil()),
 | 
					        select: sum(sg.count)
 | 
				
			||||||
        select: sum(ag.price_paid) / (sum(ag.count) + sum(sg.count))
 | 
					    ) || 0
 | 
				
			||||||
    )
 | 
					  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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -80,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.
 | 
				
			||||||
@@ -100,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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -134,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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,38 +270,240 @@ 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 am in AmmoGroup,
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
        where: am.ammo_type_id == ^ammo_type_id,
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
        where: am.user_id == ^user_id,
 | 
					        where: ag.ammo_type_id == ^ammo_type_id,
 | 
				
			||||||
        order_by: am.id
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg],
 | 
				
			||||||
 | 
					        order_by: ag.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  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
 | 
					  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 am in AmmoGroup, where: am.user_id == ^user_id, order_by: am.id
 | 
					  def list_ammo_groups(search \\ nil, include_empty \\ false, %{id: user_id}) do
 | 
				
			||||||
    else
 | 
					    from(
 | 
				
			||||||
      from am in AmmoGroup,
 | 
					      ag in AmmoGroup,
 | 
				
			||||||
        where: am.user_id == ^user_id,
 | 
					      as: :ag,
 | 
				
			||||||
        where: not (am.count == 0),
 | 
					      left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
        order_by: am.id
 | 
					      as: :sg,
 | 
				
			||||||
    end
 | 
					      join: at in assoc(ag, :ammo_type),
 | 
				
			||||||
 | 
					      as: :at,
 | 
				
			||||||
 | 
					      join: c in assoc(ag, :container),
 | 
				
			||||||
 | 
					      as: :c,
 | 
				
			||||||
 | 
					      left_join: t in assoc(c, :tags),
 | 
				
			||||||
 | 
					      as: :t,
 | 
				
			||||||
 | 
					      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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -204,10 +516,12 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
  @spec list_staged_ammo_groups(User.t()) :: [AmmoGroup.t()]
 | 
					  @spec list_staged_ammo_groups(User.t()) :: [AmmoGroup.t()]
 | 
				
			||||||
  def list_staged_ammo_groups(%User{id: user_id}) do
 | 
					  def list_staged_ammo_groups(%User{id: user_id}) do
 | 
				
			||||||
    Repo.all(
 | 
					    Repo.all(
 | 
				
			||||||
      from am in AmmoGroup,
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
        where: am.user_id == ^user_id,
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
        where: am.staged == true,
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
        order_by: am.id
 | 
					        where: ag.staged == true,
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg],
 | 
				
			||||||
 | 
					        order_by: ag.id
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -226,40 +540,156 @@ defmodule Cannery.Ammo do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_ammo_group!(AmmoGroup.id(), User.t()) :: AmmoGroup.t()
 | 
					  @spec get_ammo_group!(AmmoGroup.id(), User.t()) :: AmmoGroup.t()
 | 
				
			||||||
  def get_ammo_group!(id, %User{id: user_id}),
 | 
					  def get_ammo_group!(id, %User{id: user_id}) do
 | 
				
			||||||
    do: Repo.one!(from am in AmmoGroup, where: am.id == ^id and am.user_id == ^user_id)
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from ag in AmmoGroup,
 | 
				
			||||||
 | 
					        left_join: sg in assoc(ag, :shot_groups),
 | 
				
			||||||
 | 
					        where: ag.id == ^id,
 | 
				
			||||||
 | 
					        where: ag.user_id == ^user_id,
 | 
				
			||||||
 | 
					        preload: [shot_groups: sg]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Creates a ammo_group.
 | 
					  Returns the number of shot rounds for an ammo group
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_used_count(AmmoGroup.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_used_count(%AmmoGroup{} = ammo_group) do
 | 
				
			||||||
 | 
					    ammo_group
 | 
				
			||||||
 | 
					    |> Repo.preload(:shot_groups)
 | 
				
			||||||
 | 
					    |> Map.fetch!(:shot_groups)
 | 
				
			||||||
 | 
					    |> Enum.map(fn %{count: count} -> count end)
 | 
				
			||||||
 | 
					    |> Enum.sum()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the last entered shot group for an ammo group
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_last_used_shot_group(AmmoGroup.t()) :: ShotGroup.t() | nil
 | 
				
			||||||
 | 
					  def get_last_used_shot_group(%AmmoGroup{} = ammo_group) do
 | 
				
			||||||
 | 
					    ammo_group
 | 
				
			||||||
 | 
					    |> Repo.preload(:shot_groups)
 | 
				
			||||||
 | 
					    |> Map.fetch!(:shot_groups)
 | 
				
			||||||
 | 
					    |> Enum.max_by(fn %{date: date} -> date end, Date, fn -> nil end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Calculates the percentage remaining of an ammo group out of 100
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_percentage_remaining(AmmoGroup.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_percentage_remaining(%AmmoGroup{count: 0}), do: 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_percentage_remaining(%AmmoGroup{count: count} = ammo_group) do
 | 
				
			||||||
 | 
					    ammo_group = ammo_group |> Repo.preload(:shot_groups)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    shot_group_sum =
 | 
				
			||||||
 | 
					      ammo_group.shot_groups
 | 
				
			||||||
 | 
					      |> Enum.map(fn %{count: count} -> count end)
 | 
				
			||||||
 | 
					      |> Enum.sum()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    round(count / (count + shot_group_sum) * 100)
 | 
				
			||||||
 | 
					  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 """
 | 
				
			||||||
 | 
					  Creates multiple ammo_groups at once.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> create_ammo_group(%{field: value}, %User{id: 123})
 | 
					      iex> create_ammo_groups(%{field: value}, 3, %User{id: 123})
 | 
				
			||||||
      {:ok, %AmmoGroup{}}
 | 
					      {:ok, {3, [%AmmoGroup{}]}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> create_ammo_group(%{field: bad_value}, %User{id: 123})
 | 
					      iex> create_ammo_groups(%{field: bad_value}, 3, %User{id: 123})
 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec create_ammo_group(attrs :: map(), User.t()) ::
 | 
					  @spec create_ammo_groups(attrs :: map(), multiplier :: non_neg_integer(), User.t()) ::
 | 
				
			||||||
          {:ok, AmmoGroup.t()} | {:error, Changeset.t(AmmoGroup.new_ammo_group())}
 | 
					          {:ok, {count :: non_neg_integer(), [AmmoGroup.t()] | nil}}
 | 
				
			||||||
  def create_ammo_group(
 | 
					          | {:error, AmmoGroup.changeset()}
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
        %User{id: user_id} = user
 | 
					        multiplier,
 | 
				
			||||||
      ) do
 | 
					        %User{} = user
 | 
				
			||||||
    # validate ammo type and container ids belong to user
 | 
					      )
 | 
				
			||||||
    _valid_ammo_type = get_ammo_type!(ammo_type_id, user)
 | 
					      when multiplier >= 1 and multiplier <= @ammo_group_create_limit and
 | 
				
			||||||
    _valid_container = Containers.get_container!(container_id, user)
 | 
					             not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do
 | 
				
			||||||
 | 
					    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changesets =
 | 
				
			||||||
 | 
					      Enum.map(1..multiplier, fn _count ->
 | 
				
			||||||
        %AmmoGroup{}
 | 
					        %AmmoGroup{}
 | 
				
			||||||
    |> AmmoGroup.create_changeset(attrs |> Map.put("user_id", user_id))
 | 
					        |> AmmoGroup.create_changeset(
 | 
				
			||||||
    |> Repo.insert()
 | 
					          get_ammo_type!(ammo_type_id, user),
 | 
				
			||||||
 | 
					          Containers.get_container!(container_id, user),
 | 
				
			||||||
 | 
					          user,
 | 
				
			||||||
 | 
					          attrs
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do
 | 
				
			||||||
 | 
					      {count, inserted_ammo_groups} =
 | 
				
			||||||
 | 
					        Repo.insert_all(
 | 
				
			||||||
 | 
					          AmmoGroup,
 | 
				
			||||||
 | 
					          changesets
 | 
				
			||||||
 | 
					          |> Enum.map(fn changeset ->
 | 
				
			||||||
 | 
					            changeset
 | 
				
			||||||
 | 
					            |> Map.get(:changes)
 | 
				
			||||||
 | 
					            |> Map.merge(%{inserted_at: now, updated_at: now})
 | 
				
			||||||
 | 
					          end),
 | 
				
			||||||
 | 
					          returning: true
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:ok, {count, inserted_ammo_groups}}
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      changesets
 | 
				
			||||||
 | 
					      |> Enum.reject(fn %{valid?: valid} -> valid end)
 | 
				
			||||||
 | 
					      |> List.first()
 | 
				
			||||||
 | 
					      |> Changeset.apply_action(:insert)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create_ammo_group(invalid_attrs, _user) do
 | 
					  def create_ammo_groups(
 | 
				
			||||||
 | 
					        %{"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{}
 | 
				
			||||||
    |> AmmoGroup.create_changeset(invalid_attrs |> Map.put("user_id", "-1"))
 | 
					      |> AmmoGroup.create_changeset(
 | 
				
			||||||
    |> Repo.insert()
 | 
					        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 """
 | 
				
			||||||
@@ -275,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.
 | 
				
			||||||
@@ -292,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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -308,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: 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,10 +17,76 @@ 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}),
 | 
					  @spec list_containers(search :: nil | String.t(), User.t()) :: [Container.t()]
 | 
				
			||||||
    do: Repo.all(from c in Container, where: c.user_id == ^user_id, order_by: c.name)
 | 
					  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,
 | 
				
			||||||
 | 
					        where: c.user_id == ^user_id,
 | 
				
			||||||
 | 
					        select: count(c.id),
 | 
				
			||||||
 | 
					        distinct: true
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets a single container.
 | 
					  Gets a single container.
 | 
				
			||||||
@@ -37,8 +103,17 @@ defmodule Cannery.Containers do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_container!(Container.id(), User.t()) :: Container.t()
 | 
					  @spec get_container!(Container.id(), User.t()) :: Container.t()
 | 
				
			||||||
  def get_container!(id, %User{id: user_id}),
 | 
					  def get_container!(id, %User{id: user_id}) do
 | 
				
			||||||
    do: Repo.one!(from c in Container, where: c.id == ^id and c.user_id == ^user_id)
 | 
					    Repo.one!(
 | 
				
			||||||
 | 
					      from c in Container,
 | 
				
			||||||
 | 
					        left_join: t in assoc(c, :tags),
 | 
				
			||||||
 | 
					        left_join: ag in assoc(c, :ammo_groups),
 | 
				
			||||||
 | 
					        where: c.user_id == ^user_id,
 | 
				
			||||||
 | 
					        where: c.id == ^id,
 | 
				
			||||||
 | 
					        order_by: c.name,
 | 
				
			||||||
 | 
					        preload: [tags: t, ammo_groups: ag]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Creates a container.
 | 
					  Creates a container.
 | 
				
			||||||
@@ -53,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 """
 | 
				
			||||||
@@ -72,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
 | 
				
			||||||
@@ -90,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,
 | 
				
			||||||
@@ -105,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
 | 
				
			||||||
@@ -126,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -156,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
 | 
				
			||||||
@@ -189,4 +241,30 @@ 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 """
 | 
				
			||||||
 | 
					  Returns number of rounds in container. If data is already preloaded, then
 | 
				
			||||||
 | 
					  there will be no db hit.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_container_rounds!(Container.t()) :: non_neg_integer()
 | 
				
			||||||
 | 
					  def get_container_rounds!(%Container{} = container) do
 | 
				
			||||||
 | 
					    container
 | 
				
			||||||
 | 
					    |> Repo.preload(:ammo_groups)
 | 
				
			||||||
 | 
					    |> Map.fetch!(:ammo_groups)
 | 
				
			||||||
 | 
					    |> Enum.map(fn %{count: count} -> count end)
 | 
				
			||||||
 | 
					    |> Enum.sum()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ defmodule Cannery.Release do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def rollback(repo, version) do
 | 
					  def rollback(repo, version) do
 | 
				
			||||||
    load_app()
 | 
					    load_app()
 | 
				
			||||||
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
 | 
					    {:ok, _fun, _opts} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp load_app do
 | 
					  defp load_app do
 | 
				
			||||||
@@ -20,7 +20,7 @@ defmodule Cannery.Release do
 | 
				
			|||||||
    load_app()
 | 
					    load_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for repo <- Application.fetch_env!(@app, :ecto_repos) do
 | 
					    for repo <- Application.fetch_env!(@app, :ecto_repos) do
 | 
				
			||||||
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
 | 
					      {:ok, _fun, _opts} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,11 +6,11 @@ defmodule Cannery.Repo.Migrator do
 | 
				
			|||||||
  use GenServer
 | 
					  use GenServer
 | 
				
			||||||
  require Logger
 | 
					  require Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def start_link(_) do
 | 
					  def start_link(_opts) do
 | 
				
			||||||
    GenServer.start_link(__MODULE__, [], [])
 | 
					    GenServer.start_link(__MODULE__, [], [])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def init(_) do
 | 
					  def init(_opts) do
 | 
				
			||||||
    migrate!()
 | 
					    migrate!()
 | 
				
			||||||
    {:ok, nil}
 | 
					    {:ok, nil}
 | 
				
			||||||
  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
 | 
				
			||||||
@@ -71,6 +72,7 @@ defmodule CanneryWeb do
 | 
				
			|||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
      use Phoenix.Router
 | 
					      use Phoenix.Router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      import Plug.Conn
 | 
					      import Plug.Conn
 | 
				
			||||||
      import Phoenix.Controller
 | 
					      import Phoenix.Controller
 | 
				
			||||||
      import Phoenix.LiveView.Router
 | 
					      import Phoenix.LiveView.Router
 | 
				
			||||||
@@ -79,7 +81,9 @@ defmodule CanneryWeb do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def channel do
 | 
					  def channel do
 | 
				
			||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      use Phoenix.Channel
 | 
					      use Phoenix.Channel
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      import CanneryWeb.Gettext
 | 
					      import CanneryWeb.Gettext
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -87,15 +91,20 @@ defmodule CanneryWeb do
 | 
				
			|||||||
  defp view_helpers do
 | 
					  defp view_helpers do
 | 
				
			||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
      # Use all HTML functionality (forms, tags, etc)
 | 
					      # Use all HTML functionality (forms, tags, etc)
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      use Phoenix.HTML
 | 
					      use Phoenix.HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
 | 
					      # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
 | 
				
			||||||
      import Phoenix.LiveView.Helpers
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
      import Phoenix.View
 | 
					      import Phoenix.View
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # 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,10 +4,10 @@
 | 
				
			|||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="shot-group-form"
 | 
					    id="shot-group-form"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
@@ -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,8 +42,12 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= 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",
 | 
				
			||||||
 | 
					      phx_update: "ignore",
 | 
				
			||||||
 | 
					      value: Date.utc_today()
 | 
				
			||||||
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :notes, "col-span-3") %>
 | 
					    <%= error_tag(f, :notes, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= submit(dgettext("actions", "Save"),
 | 
					    <%= submit(dgettext("actions", "Save"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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:") %>
 | 
				
			||||||
 | 
					          <.date date={@ammo_group.purchased_on} />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= if @ammo_group |> Ammo.get_last_used_shot_group() do %>
 | 
				
			||||||
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					            <%= gettext("Last used on:") %>
 | 
				
			||||||
 | 
					            <.date date={@ammo_group |> Ammo.get_last_used_shot_group() |> Map.get(: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"""
 | 
				
			||||||
 | 
					     <.date date={@purchased_on} />
 | 
				
			||||||
 | 
					     """}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_value_for_key(:used_up_on, ammo_group, _additional_data) do
 | 
				
			||||||
 | 
					    last_shot_group_date =
 | 
				
			||||||
 | 
					      case ammo_group |> Ammo.get_last_used_shot_group() do
 | 
				
			||||||
 | 
					        %{date: last_shot_group_date} -> last_shot_group_date
 | 
				
			||||||
 | 
					        _no_shot_groups -> nil
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assigns = %{last_shot_group_date: last_shot_group_date}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {last_shot_group_date,
 | 
				
			||||||
 | 
					     ~H"""
 | 
				
			||||||
 | 
					     <%= if @last_shot_group_date do %>
 | 
				
			||||||
 | 
					       <.date date={@last_shot_group_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
 | 
				
			||||||
@@ -4,23 +4,35 @@ defmodule CanneryWeb.Components.ContainerCard do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :component
 | 
					  use CanneryWeb, :component
 | 
				
			||||||
 | 
					  import CanneryWeb.Components.TagCard
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					    assigns =
 | 
				
			||||||
 | 
					      assigns
 | 
				
			||||||
 | 
					      |> assign(container: container |> Repo.preload([:tags, :ammo_groups]))
 | 
				
			||||||
 | 
					      |> assign_new(:tag_actions, fn -> [] end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def container_card(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      id={"container-#{@container.id}"}
 | 
					      id={"container-#{@container.id}"}
 | 
				
			||||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center
 | 
					      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="mb-4 flex flex-col justify-center items-center">
 | 
					      <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">
 | 
				
			||||||
@@ -40,6 +52,28 @@ defmodule CanneryWeb.Components.ContainerCard do
 | 
				
			|||||||
            <%= @container.location %>
 | 
					            <%= @container.location %>
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= unless @container.ammo_groups |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					            <%= gettext("Packs:") %>
 | 
				
			||||||
 | 
					            <%= @container |> Containers.get_container_ammo_group_count!() %>
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					            <%= gettext("Rounds:") %>
 | 
				
			||||||
 | 
					            <%= @container |> Containers.get_container_rounds!() %>
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
 | 
					          <%= unless @container.tags |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					            <%= for tag <- @container.tags do %>
 | 
				
			||||||
 | 
					              <.simple_tag_card tag={tag} />
 | 
				
			||||||
 | 
					            <% end %>
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <%= render_slot(@tag_actions) %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= if assigns |> Map.has_key?(:inner_block) do %>
 | 
					      <%= if assigns |> Map.has_key?(:inner_block) do %>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,11 +4,19 @@ 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
 | 
					    <div
 | 
				
			||||||
 | 
					      id={"invite-#{@invite.id}"}
 | 
				
			||||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
					      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"
 | 
				
			||||||
@@ -19,8 +27,14 @@ defmodule CanneryWeb.Components.InviteCard do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <%= if @invite.disabled_at |> is_nil() do %>
 | 
					      <%= if @invite.disabled_at |> is_nil() do %>
 | 
				
			||||||
        <h2 class="title text-md">
 | 
					        <h2 class="title text-md">
 | 
				
			||||||
          <%= gettext("Uses Left:") %>
 | 
					          <%= if @invite.uses_left do %>
 | 
				
			||||||
          <%= @invite.uses_left || "Unlimited" %>
 | 
					            <%= gettext(
 | 
				
			||||||
 | 
					              "Uses Left: %{uses_left}",
 | 
				
			||||||
 | 
					              uses_left: @invite.uses_left
 | 
				
			||||||
 | 
					            ) %>
 | 
				
			||||||
 | 
					          <% else %>
 | 
				
			||||||
 | 
					            <%= gettext("Uses Left: Unlimited") %>
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
        </h2>
 | 
					        </h2>
 | 
				
			||||||
      <% else %>
 | 
					      <% else %>
 | 
				
			||||||
        <h2 class="title text-md">
 | 
					        <h2 class="title text-md">
 | 
				
			||||||
@@ -28,17 +42,18 @@ defmodule CanneryWeb.Components.InviteCard do
 | 
				
			|||||||
        </h2>
 | 
					        </h2>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <.qr_code
 | 
				
			||||||
 | 
					        content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
 | 
				
			||||||
 | 
					        filename={@invite.name}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
					      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
				
			||||||
        <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>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <%= if @code_actions do %>
 | 
					 | 
				
			||||||
        <%= render_slot(@code_actions) %>
 | 
					        <%= render_slot(@code_actions) %>
 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= if @inner_block do %>
 | 
					      <%= if @inner_block do %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,8 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_component
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
  alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Containers}
 | 
					  alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Containers, Containers.Container}
 | 
				
			||||||
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -21,13 +22,18 @@ 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)
 | 
				
			||||||
      |> Enum.reject(fn %{id: id} -> id == container_id end)
 | 
					      |> Enum.reject(fn %{id: id} -> id == container_id end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(changeset: changeset, containers: containers)}
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> assign(changeset: changeset, containers: containers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:ok, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -46,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)
 | 
				
			||||||
@@ -54,4 +60,75 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def render(%{containers: containers} = assigns) do
 | 
				
			||||||
 | 
					    columns = [
 | 
				
			||||||
 | 
					      %{label: gettext("Container"), key: "name"},
 | 
				
			||||||
 | 
					      %{label: gettext("Type"), key: "type"},
 | 
				
			||||||
 | 
					      %{label: gettext("Location"), key: "location"},
 | 
				
			||||||
 | 
					      %{label: nil, key: "actions", sortable: false}
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows = containers |> get_rows_for_containers(assigns, columns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assigns = assigns |> Map.merge(%{columns: columns, rows: rows})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <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">
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Move ammo") %>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <%= if @containers |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					        <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					          <%= gettext("No other containers") %>
 | 
				
			||||||
 | 
					          <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					        </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary">
 | 
				
			||||||
 | 
					          <%= dgettext("actions", "Add another container!") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <.live_component
 | 
				
			||||||
 | 
					          module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
 | 
					          id="move_ammo_group_table"
 | 
				
			||||||
 | 
					          columns={@columns}
 | 
				
			||||||
 | 
					          rows={@rows}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_rows_for_containers([Container.t()], map(), [map()]) :: [map()]
 | 
				
			||||||
 | 
					  defp get_rows_for_containers(containers, assigns, columns) do
 | 
				
			||||||
 | 
					    containers
 | 
				
			||||||
 | 
					    |> Enum.map(fn container ->
 | 
				
			||||||
 | 
					      columns
 | 
				
			||||||
 | 
					      |> Map.new(fn %{key: key} -> {key, get_row_value_by_key(key, container, assigns)} end)
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_row_value_by_key(String.t(), Container.t(), map()) :: any()
 | 
				
			||||||
 | 
					  defp get_row_value_by_key("actions", container, assigns) do
 | 
				
			||||||
 | 
					    assigns = assigns |> Map.put(:container, container)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        type="button"
 | 
				
			||||||
 | 
					        class="btn btn-primary"
 | 
				
			||||||
 | 
					        phx-click="move"
 | 
				
			||||||
 | 
					        phx-target={@myself}
 | 
				
			||||||
 | 
					        phx-value-container_id={@container.id}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= dgettext("actions", "Select") %>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_row_value_by_key(key, container, _assigns),
 | 
				
			||||||
 | 
					    do: container |> Map.get(key |> String.to_existing_atom())
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,70 +0,0 @@
 | 
				
			|||||||
<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">
 | 
					 | 
				
			||||||
    <%= gettext("Move ammo") %>
 | 
					 | 
				
			||||||
  </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <%= if @containers |> Enum.empty?() do %>
 | 
					 | 
				
			||||||
    <h2 class="title text-xl text-primary-600">
 | 
					 | 
				
			||||||
      <%= gettext("No other containers") %>
 | 
					 | 
				
			||||||
      <%= display_emoji("😔") %>
 | 
					 | 
				
			||||||
    </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Add another container!"),
 | 
					 | 
				
			||||||
      to: Routes.container_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
      class: "btn btn-primary"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					 | 
				
			||||||
    <div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
 | 
					 | 
				
			||||||
      <table class="min-w-full table-auto text-center bg-white">
 | 
					 | 
				
			||||||
        <thead class="border-b border-primary-600">
 | 
					 | 
				
			||||||
          <tr>
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Container") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Type") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Location") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <th class="p-2"></th>
 | 
					 | 
				
			||||||
          </tr>
 | 
					 | 
				
			||||||
        </thead>
 | 
					 | 
				
			||||||
        <tbody id="containers">
 | 
					 | 
				
			||||||
          <%= for container <- @containers do %>
 | 
					 | 
				
			||||||
            <tr id={"container-#{container.id}"}>
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= container.name %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= container.type %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= container.location %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
                  <button
 | 
					 | 
				
			||||||
                    type="button"
 | 
					 | 
				
			||||||
                    class="btn btn-primary"
 | 
					 | 
				
			||||||
                    phx-click="move"
 | 
					 | 
				
			||||||
                    phx-target={@myself}
 | 
					 | 
				
			||||||
                    phx-value-container_id={container.id}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <%= dgettext("actions", "Select") %>
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </tbody>
 | 
					 | 
				
			||||||
      </table>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
							
								
								
									
										121
									
								
								lib/cannery_web/components/shot_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								lib/cannery_web/components/shot_group_table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					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, assigns = %{date: _date}, _extra_data) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <.date date={@date} />
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
							
								
								
									
										111
									
								
								lib/cannery_web/components/table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								lib/cannery_web/components/table_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.Components.TableComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Livecomponent that presents a resortable table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  It takes the following required assigns:
 | 
				
			||||||
 | 
					    - `:columns`: An array of maps containing the following keys
 | 
				
			||||||
 | 
					      - `:label`: A gettext'd or otherwise user-facing string label for the
 | 
				
			||||||
 | 
					        column. Can be nil
 | 
				
			||||||
 | 
					      - `:key`: An atom key used for sorting
 | 
				
			||||||
 | 
					      - `:class`: Extra classes to be applied to the column element, if desired.
 | 
				
			||||||
 | 
					        Optional
 | 
				
			||||||
 | 
					      - `:sortable`: If false, will prevent the user from sorting with it.
 | 
				
			||||||
 | 
					        Optional
 | 
				
			||||||
 | 
					    - `:values`: An array of maps containing data for each row. Each map is
 | 
				
			||||||
 | 
					      string-keyed with the associated column key to the following values:
 | 
				
			||||||
 | 
					      - A single element, like string, integer or Phoenix.LiveView.Rendered
 | 
				
			||||||
 | 
					        object, like returned from the ~H sigil
 | 
				
			||||||
 | 
					      - A tuple, containing a custom value used for sorting, and the displayed
 | 
				
			||||||
 | 
					        content.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					  require Integer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{
 | 
				
			||||||
 | 
					            required(:columns) =>
 | 
				
			||||||
 | 
					              list(%{
 | 
				
			||||||
 | 
					                required(:label) => String.t() | nil,
 | 
				
			||||||
 | 
					                required(:key) => atom() | nil,
 | 
				
			||||||
 | 
					                optional(:class) => String.t(),
 | 
				
			||||||
 | 
					                optional(:row_class) => String.t(),
 | 
				
			||||||
 | 
					                optional(:alternate_row_class) => String.t(),
 | 
				
			||||||
 | 
					                optional(:sortable) => false
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					            required(:rows) =>
 | 
				
			||||||
 | 
					              list(%{
 | 
				
			||||||
 | 
					                (key :: atom()) => any() | {custom_sort_value :: String.t(), value :: any()}
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					            optional(:inital_key) => atom(),
 | 
				
			||||||
 | 
					            optional(:initial_sort_mode) => atom(),
 | 
				
			||||||
 | 
					            optional(any()) => any()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{columns: columns, rows: rows} = assigns, socket) do
 | 
				
			||||||
 | 
					    initial_key =
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					      |> assign(assigns)
 | 
				
			||||||
 | 
					      |> 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}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event(
 | 
				
			||||||
 | 
					        "sort_by",
 | 
				
			||||||
 | 
					        %{"sort-key" => key},
 | 
				
			||||||
 | 
					        %{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    key = key |> String.to_existing_atom()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sort_mode =
 | 
				
			||||||
 | 
					      case {key, sort_mode} do
 | 
				
			||||||
 | 
					        {^last_sort_key, :asc} -> :desc
 | 
				
			||||||
 | 
					        {^last_sort_key, :desc} -> :asc
 | 
				
			||||||
 | 
					        {_new_sort_key, _last_sort_mode} -> :asc
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
 | 
				
			||||||
 | 
					    rows
 | 
				
			||||||
 | 
					    |> Enum.sort_by(
 | 
				
			||||||
 | 
					      fn row ->
 | 
				
			||||||
 | 
					        case row |> Map.get(key) do
 | 
				
			||||||
 | 
					          {custom_sort_key, _value} -> custom_sort_key
 | 
				
			||||||
 | 
					          value -> value
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end,
 | 
				
			||||||
 | 
					      sort_mode
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										53
									
								
								lib/cannery_web/components/table_component.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/cannery_web/components/table_component.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					<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">
 | 
				
			||||||
 | 
					    <thead class="border-b border-primary-600">
 | 
				
			||||||
 | 
					      <tr>
 | 
				
			||||||
 | 
					        <%= for %{key: key, label: label} = column <- @columns do %>
 | 
				
			||||||
 | 
					          <%= if column |> Map.get(:sortable, true) do %>
 | 
				
			||||||
 | 
					            <th class={["p-2", column[:class]]}>
 | 
				
			||||||
 | 
					              <span
 | 
				
			||||||
 | 
					                class="cursor-pointer flex justify-center items-center space-x-2"
 | 
				
			||||||
 | 
					                phx-click="sort_by"
 | 
				
			||||||
 | 
					                phx-value-sort-key={key}
 | 
				
			||||||
 | 
					                phx-target={@myself}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <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 %>
 | 
				
			||||||
 | 
					                  <%= case @sort_mode do %>
 | 
				
			||||||
 | 
					                    <% :asc -> %>
 | 
				
			||||||
 | 
					                      <i class="w-0 float-right fas fa-sm fa-chevron-down"></i>
 | 
				
			||||||
 | 
					                    <% :desc -> %>
 | 
				
			||||||
 | 
					                      <i class="w-0 float-right fas fa-sm fa-chevron-up"></i>
 | 
				
			||||||
 | 
					                  <% end %>
 | 
				
			||||||
 | 
					                <% else %>
 | 
				
			||||||
 | 
					                  <i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
 | 
				
			||||||
 | 
					                <% end %>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
 | 
					          <% else %>
 | 
				
			||||||
 | 
					            <th class={["p-2 cursor-not-allowed", column[:class]]}>
 | 
				
			||||||
 | 
					              <%= label %>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody>
 | 
				
			||||||
 | 
					      <%= for {values, i} <- @rows |> Enum.with_index() do %>
 | 
				
			||||||
 | 
					        <tr class={if i |> Integer.is_even(), do: @row_class, else: @alternate_row_class}>
 | 
				
			||||||
 | 
					          <%= for %{key: key} = value <- @columns do %>
 | 
				
			||||||
 | 
					            <td class={["p-2", value[:class]]}>
 | 
				
			||||||
 | 
					              <%= case values |> Map.get(key) do %>
 | 
				
			||||||
 | 
					                <% {_custom_sort_value, value} -> %>
 | 
				
			||||||
 | 
					                  <%= value %>
 | 
				
			||||||
 | 
					                <% value -> %>
 | 
				
			||||||
 | 
					                  <%= value %>
 | 
				
			||||||
 | 
					              <% end %>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </tbody>
 | 
				
			||||||
 | 
					  </table>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -4,24 +4,35 @@ 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"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <h1
 | 
					      <.simple_tag_card tag={@tag} />
 | 
				
			||||||
        class="px-4 py-2 rounded-lg title text-xl"
 | 
					 | 
				
			||||||
        style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <%= @tag.name %>
 | 
					 | 
				
			||||||
      </h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= render_slot(@inner_block) %>
 | 
					      <%= render_slot(@inner_block) %>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :tag, Tag, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def simple_tag_card(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <h1
 | 
				
			||||||
 | 
					      class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
 | 
				
			||||||
 | 
					      style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= @tag.name %>
 | 
				
			||||||
 | 
					    </h1>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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">
 | 
				
			||||||
@@ -31,84 +38,104 @@ defmodule CanneryWeb.Components.Topbar do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <hr class="mb-2 sm:hidden hr-light" />
 | 
					        <hr class="mb-2 sm:hidden hr-light" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ul
 | 
					        <ul class="flex flex-row flex-wrap justify-center items-center
 | 
				
			||||||
          class="flex flex-row flex-wrap justify-center items-center
 | 
					          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,12 +4,16 @@ 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"""
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      id={"user-#{@user.id}"}
 | 
					      id={"user-#{@user.id}"}
 | 
				
			||||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center
 | 
					      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
 | 
				
			||||||
          border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
					          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"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
@@ -18,11 +22,25 @@ 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 do %>
 | 
				
			||||||
 | 
					            <%= gettext(
 | 
				
			||||||
 | 
					              "User was confirmed at%{confirmed_datetime}",
 | 
				
			||||||
 | 
					              confirmed_datetime: ""
 | 
				
			||||||
 | 
					            ) %>
 | 
				
			||||||
 | 
					            <.datetime datetime={@user.confirmed_at} />
 | 
				
			||||||
          <% else %>
 | 
					          <% else %>
 | 
				
			||||||
          User was confirmed at<%= @user.confirmed_at |> display_datetime() %>
 | 
					            <%= gettext("Email unconfirmed") %>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>
 | 
				
			||||||
 | 
					          <%= gettext(
 | 
				
			||||||
 | 
					            "User registered on%{registered_datetime}",
 | 
				
			||||||
 | 
					            registered_datetime: ""
 | 
				
			||||||
 | 
					          ) %>
 | 
				
			||||||
 | 
					          <.datetime datetime={@user.inserted_at} />
 | 
				
			||||||
 | 
					        </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
 | 
				
			||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
defmodule CanneryWeb.UserConfirmationController do
 | 
					defmodule CanneryWeb.UserConfirmationController do
 | 
				
			||||||
  use CanneryWeb, :controller
 | 
					  use CanneryWeb, :controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  alias Cannery.Accounts
 | 
					  alias Cannery.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html")
 | 
					    render(conn, "new.html", page_title: gettext("Confirm your account"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
					  def create(conn, %{"user" => %{"email" => email}}) do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
@@ -29,8 +28,11 @@ 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
 | 
				
			||||||
    changeset = Accounts.change_user_registration(%User{})
 | 
					    render(conn, "new.html",
 | 
				
			||||||
    conn |> render("new.html", changeset: changeset, invite: invite)
 | 
					      changeset: Accounts.change_user_registration(),
 | 
				
			||||||
 | 
					      invite: invite,
 | 
				
			||||||
 | 
					      page_title: gettext("Register")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
 | 
					  def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ defmodule CanneryWeb.UserResetPasswordController do
 | 
				
			|||||||
  plug :get_user_by_reset_password_token when action in [:edit, :update]
 | 
					  plug :get_user_by_reset_password_token when action in [:edit, :update]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html")
 | 
					    render(conn, "new.html", page_title: gettext("Forgot your password?"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
					  def create(conn, %{"user" => %{"email" => email}}) do
 | 
				
			||||||
@@ -31,7 +31,10 @@ defmodule CanneryWeb.UserResetPasswordController do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit(conn, _params) do
 | 
					  def edit(conn, _params) do
 | 
				
			||||||
    render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user))
 | 
					    render(conn, "edit.html",
 | 
				
			||||||
 | 
					      changeset: Accounts.change_user_password(conn.assigns.user),
 | 
				
			||||||
 | 
					      page_title: gettext("Reset your password")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Do not log in the user after reset password to avoid a
 | 
					  # Do not log in the user after reset password to avoid a
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ defmodule CanneryWeb.UserSessionController do
 | 
				
			|||||||
  alias CanneryWeb.UserAuth
 | 
					  alias CanneryWeb.UserAuth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html", error_message: nil)
 | 
					    render(conn, "new.html", error_message: nil, page_title: gettext("Log in"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => user_params}) do
 | 
					  def create(conn, %{"user" => user_params}) do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,20 @@
 | 
				
			|||||||
defmodule CanneryWeb.UserSettingsController do
 | 
					defmodule CanneryWeb.UserSettingsController do
 | 
				
			||||||
  use CanneryWeb, :controller
 | 
					  use CanneryWeb, :controller
 | 
				
			||||||
 | 
					  import CanneryWeb.Gettext
 | 
				
			||||||
  alias Cannery.Accounts
 | 
					  alias Cannery.Accounts
 | 
				
			||||||
  alias CanneryWeb.{HomeLive, UserAuth}
 | 
					  alias CanneryWeb.{HomeLive, UserAuth}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  plug :assign_email_and_password_changesets
 | 
					  plug :assign_email_and_password_changesets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit(conn, _params) do
 | 
					  def edit(conn, _params) do
 | 
				
			||||||
    render(conn, "edit.html")
 | 
					    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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
  alias Ecto.Changeset
 | 
					  alias Ecto.Changeset
 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ammo_group_create_limit 10_000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  @spec update(
 | 
					  @spec update(
 | 
				
			||||||
          %{:ammo_group => AmmoGroup.t(), :current_user => User.t(), optional(any) => any},
 | 
					          %{:ammo_group => AmmoGroup.t(), :current_user => User.t(), optional(any) => any},
 | 
				
			||||||
@@ -19,21 +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
 | 
				
			||||||
    changeset = Ammo.change_ammo_group(ammo_group)
 | 
					    %{assigns: %{ammo_types: ammo_types, containers: containers}} =
 | 
				
			||||||
    containers = Containers.list_containers(current_user)
 | 
					      socket =
 | 
				
			||||||
    ammo_types = Ammo.list_ammo_types(current_user)
 | 
					      socket
 | 
				
			||||||
    {:ok, socket |> assign(changeset: changeset, containers: containers, ammo_types: ammo_types)}
 | 
					      |> assign(:ammo_group_create_limit, @ammo_group_create_limit)
 | 
				
			||||||
 | 
					      |> assign(:ammo_types, Ammo.list_ammo_types(current_user))
 | 
				
			||||||
 | 
					      |> assign_new(:containers, fn -> Containers.list_containers(current_user) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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: %{ammo_group: ammo_group}} = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    socket = socket |> assign(:changeset, ammo_group |> Ammo.change_ammo_group(ammo_group_params))
 | 
					 | 
				
			||||||
    {:noreply, socket}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
@@ -57,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,
 | 
				
			||||||
@@ -66,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)
 | 
				
			||||||
@@ -77,20 +126,66 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp save_ammo_group(
 | 
					  defp save_ammo_group(
 | 
				
			||||||
         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
					         %{assigns: %{changeset: changeset}} = socket,
 | 
				
			||||||
         :new,
 | 
					         action,
 | 
				
			||||||
         ammo_group_params
 | 
					         %{"multiplier" => multiplier_str} = ammo_group_params
 | 
				
			||||||
       ) do
 | 
					       )
 | 
				
			||||||
 | 
					       when action in [:new, :clone] do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      case Ammo.create_ammo_group(ammo_group_params, current_user) do
 | 
					      case multiplier_str |> Integer.parse() do
 | 
				
			||||||
        {:ok, _ammo_group} ->
 | 
					        {multiplier, _remainder}
 | 
				
			||||||
          prompt = dgettext("prompts", "Ammo group created successfully")
 | 
					        when multiplier >= 1 and multiplier <= @ammo_group_create_limit ->
 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					          socket |> create_multiple(ammo_group_params, multiplier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:error, %Changeset{} = changeset} ->
 | 
					        {multiplier, _remainder} ->
 | 
				
			||||||
          socket |> assign(changeset: changeset)
 | 
					          error_msg =
 | 
				
			||||||
 | 
					            dgettext(
 | 
				
			||||||
 | 
					              "errors",
 | 
				
			||||||
 | 
					              "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}",
 | 
				
			||||||
 | 
					              max: @ammo_group_create_limit,
 | 
				
			||||||
 | 
					              multiplier: multiplier
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {:error, changeset} =
 | 
				
			||||||
 | 
					            changeset
 | 
				
			||||||
 | 
					            |> Changeset.add_error(:multiplier, error_msg)
 | 
				
			||||||
 | 
					            |> Changeset.apply_action(:insert)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :error ->
 | 
				
			||||||
 | 
					          error_msg = dgettext("errors", "Could not parse number of copies")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {:error, changeset} =
 | 
				
			||||||
 | 
					            changeset
 | 
				
			||||||
 | 
					            |> Changeset.add_error(:multiplier, error_msg)
 | 
				
			||||||
 | 
					            |> Changeset.apply_action(:insert)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          socket |> assign(:changeset, changeset)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp create_multiple(
 | 
				
			||||||
 | 
					         %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
 | 
				
			||||||
 | 
					         ammo_group_params,
 | 
				
			||||||
 | 
					         multiplier
 | 
				
			||||||
 | 
					       ) do
 | 
				
			||||||
 | 
					    case Ammo.create_ammo_groups(ammo_group_params, multiplier, current_user) do
 | 
				
			||||||
 | 
					      {:ok, {count, _ammo_groups}} ->
 | 
				
			||||||
 | 
					        prompt =
 | 
				
			||||||
 | 
					          dngettext(
 | 
				
			||||||
 | 
					            "prompts",
 | 
				
			||||||
 | 
					            "Ammo added successfully",
 | 
				
			||||||
 | 
					            "Ammo added successfully",
 | 
				
			||||||
 | 
					            count
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:error, %Changeset{} = changeset} ->
 | 
				
			||||||
 | 
					        socket |> assign(changeset: changeset)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,13 @@
 | 
				
			|||||||
  </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}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= if @changeset.action && not @changeset.valid? do %>
 | 
					    <%= if @changeset.action && not @changeset.valid? do %>
 | 
				
			||||||
      <div class="invalid-feedback col-span-3 text-center">
 | 
					      <div class="invalid-feedback col-span-3 text-center">
 | 
				
			||||||
@@ -18,42 +18,70 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :ammo_type_id, gettext("Ammo type"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :ammo_type_id, gettext("Ammo type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= select(f, :ammo_type_id, ammo_type_options(@ammo_types),
 | 
					    <%= select(f, :ammo_type_id, ammo_type_options(@ammo_types),
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary"
 | 
					      class: "text-center col-span-2 input input-primary"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :ammo_type_id, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :ammo_type_id, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :count, gettext("Count"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :count, gettext("Count"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= number_input(f, :count,
 | 
					    <%= number_input(f, :count,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      min: 1
 | 
					      min: 0
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :count, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :count, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :price_paid, gettext("Price paid"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :price_paid, gettext("Price paid"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= number_input(f, :price_paid,
 | 
					    <%= number_input(f, :price_paid,
 | 
				
			||||||
      step: "0.01",
 | 
					      step: 0.01,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary"
 | 
					      class: "text-center col-span-2 input input-primary"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :price_paid, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :price_paid, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :notes, gettext("Notes"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= 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") %>
 | 
				
			||||||
    <%= textarea(f, :notes,
 | 
					    <%= textarea(f, :notes,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      phx_hook: "MaintainAttrs"
 | 
					      phx_hook: "MaintainAttrs"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :notes, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :notes, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :container, gettext("Container"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :container, gettext("Container"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= select(f, :container_id, container_options(@containers),
 | 
					    <%= select(f, :container_id, container_options(@containers),
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary"
 | 
					      class: "text-center col-span-2 input input-primary"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :container_id, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :container_id, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= cond do %>
 | 
				
			||||||
 | 
					      <% @action in [:new, :clone] -> %>
 | 
				
			||||||
 | 
					        <hr class="hr col-span-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
 | 
					        <%= number_input(f, :multiplier,
 | 
				
			||||||
 | 
					          max: @ammo_group_create_limit,
 | 
				
			||||||
 | 
					          class: "text-center input input-primary",
 | 
				
			||||||
 | 
					          value: 1,
 | 
				
			||||||
 | 
					          phx_update: "ignore"
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= submit(dgettext("actions", "Create"),
 | 
				
			||||||
 | 
					          phx_disable_with: dgettext("prompts", "Creating..."),
 | 
				
			||||||
 | 
					          class: "mx-auto btn btn-primary"
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <%= error_tag(f, :multiplier, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					      <% @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"
 | 
				
			||||||
        ) %>
 | 
					        ) %>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
  </.form>
 | 
					  </.form>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,54 +4,89 @@ defmodule CanneryWeb.AmmoGroupLive.Index do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  alias Cannery.{Ammo, Ammo.AmmoGroup, 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(%{assigns: %{current_user: current_user}} = socket, :add_shot_group, %{
 | 
					  defp apply_action(
 | 
				
			||||||
         "id" => id
 | 
					         %{assigns: %{current_user: current_user}} = socket,
 | 
				
			||||||
       }) do
 | 
					         :add_shot_group,
 | 
				
			||||||
 | 
					         %{"id" => id}
 | 
				
			||||||
 | 
					       ) 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
 | 
				
			||||||
@@ -70,8 +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
 | 
				
			||||||
    socket |> assign(:ammo_groups, ammo_groups)
 | 
					    {:noreply, socket |> assign(:show_used, !show_used) |> display_ammo_groups()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :index))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					      ammo_types_count: ammo_types_count,
 | 
				
			||||||
 | 
					      containers_count: containers_count
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,137 +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>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= live_patch(dgettext("actions", "Add your first box!"),
 | 
					 | 
				
			||||||
      to: Routes.ammo_group_index_path(Endpoint, :new),
 | 
					 | 
				
			||||||
      class: "btn btn-primary"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  <% else %>
 | 
					  <% else %>
 | 
				
			||||||
    <%= live_patch(dgettext("actions", "New Ammo group"),
 | 
					    <.live_component
 | 
				
			||||||
      to: Routes.ammo_group_index_path(Endpoint, :new),
 | 
					      module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
      class: "btn btn-primary"
 | 
					      id="ammo-group-index-table"
 | 
				
			||||||
    ) %>
 | 
					      ammo_groups={@ammo_groups}
 | 
				
			||||||
 | 
					      current_user={@current_user}
 | 
				
			||||||
    <div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
 | 
					    >
 | 
				
			||||||
      <table class="min-w-full table-auto text-center bg-white">
 | 
					      <:ammo_type :let={%{name: ammo_type_name} = ammo_type}>
 | 
				
			||||||
        <thead class="border-b border-primary-600">
 | 
					        <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link">
 | 
				
			||||||
          <tr>
 | 
					          <%= ammo_type_name %>
 | 
				
			||||||
            <th class="p-2">
 | 
					        </.link>
 | 
				
			||||||
              <%= gettext("Ammo type") %>
 | 
					      </:ammo_type>
 | 
				
			||||||
            </th>
 | 
					      <:range :let={ammo_group}>
 | 
				
			||||||
            <th class="p-2">
 | 
					        <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center">
 | 
				
			||||||
              <%= gettext("Count") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Price paid") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Notes") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Range") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Container") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <th class="p-2"></th>
 | 
					 | 
				
			||||||
          </tr>
 | 
					 | 
				
			||||||
        </thead>
 | 
					 | 
				
			||||||
        <tbody id="ammo_groups">
 | 
					 | 
				
			||||||
          <%= for ammo_group <- @ammo_groups do %>
 | 
					 | 
				
			||||||
            <tr id={"ammo_group-#{ammo_group.id}"}>
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= live_patch(ammo_group.ammo_type.name,
 | 
					 | 
				
			||||||
                  to: Routes.ammo_type_show_path(Endpoint, :show, ammo_group.ammo_type),
 | 
					 | 
				
			||||||
                  class: "link"
 | 
					 | 
				
			||||||
                ) %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= ammo_group.count %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= if ammo_group.price_paid do %>
 | 
					 | 
				
			||||||
                  <%= gettext("$%{amount}",
 | 
					 | 
				
			||||||
                    amount: ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
 | 
					 | 
				
			||||||
                  ) %>
 | 
					 | 
				
			||||||
                <% end %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= ammo_group.notes %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					 | 
				
			||||||
          <button
 | 
					          <button
 | 
				
			||||||
            type="button"
 | 
					            type="button"
 | 
				
			||||||
                    class="btn btn-primary"
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
            phx-click="toggle_staged"
 | 
					            phx-click="toggle_staged"
 | 
				
			||||||
            phx-value-ammo_group_id={ammo_group.id}
 | 
					            phx-value-ammo_group_id={ammo_group.id}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <%= if ammo_group.staged, do: gettext("Unstage"), else: gettext("Stage") %>
 | 
					            <%= if ammo_group.staged, do: gettext("Unstage"), else: gettext("Stage") %>
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <%= live_patch(dgettext("actions", "Record shots"),
 | 
					          <.link
 | 
				
			||||||
                    to: Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group),
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group)}
 | 
				
			||||||
                    class: "btn btn-primary"
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
                  ) %>
 | 
					          >
 | 
				
			||||||
 | 
					            <%= dgettext("actions", "Record shots") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
              </td>
 | 
					      </: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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <td class="p-2">
 | 
					          <.link
 | 
				
			||||||
                <%= if ammo_group.container do %>
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)}
 | 
				
			||||||
                  <%= live_patch(ammo_group.container.name,
 | 
					            class="mx-2 my-1 text-sm btn btn-primary"
 | 
				
			||||||
                    to: Routes.ammo_group_index_path(Endpoint, :move, ammo_group),
 | 
					          >
 | 
				
			||||||
                    class: "btn btn-primary"
 | 
					            <%= gettext("Move ammo") %>
 | 
				
			||||||
                  ) %>
 | 
					          </.link>
 | 
				
			||||||
                <% end %>
 | 
					        </div>
 | 
				
			||||||
              </td>
 | 
					      </:container>
 | 
				
			||||||
 | 
					      <:actions :let={ammo_group}>
 | 
				
			||||||
              <td class="p-2">
 | 
					        <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center">
 | 
				
			||||||
                <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
					          <.link
 | 
				
			||||||
                  <%= live_redirect to: Routes.ammo_group_show_path(Endpoint, :show, ammo_group),
 | 
					            navigate={Routes.ammo_group_show_path(Endpoint, :show, ammo_group)}
 | 
				
			||||||
                                class: "text-primary-600 link",
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
                                data: [qa: "view-#{ammo_group.id}"] do %>
 | 
					            data-qa={"view-#{ammo_group.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
					            <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
				
			||||||
                  <% end %>
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <%= live_patch to: Routes.ammo_group_index_path(Endpoint, :edit, ammo_group),
 | 
					          <.link
 | 
				
			||||||
                             class: "text-primary-600 link",
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)}
 | 
				
			||||||
                             data: [qa: "edit-#{ammo_group.id}"] do %>
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            data-qa={"edit-#{ammo_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",
 | 
					            patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)}
 | 
				
			||||||
                       phx_click: "delete",
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
                       phx_value_id: ammo_group.id,
 | 
					            data-qa={"clone-#{ammo_group.id}"}
 | 
				
			||||||
                       data: [
 | 
					          >
 | 
				
			||||||
                         confirm: dgettext("prompts", "Are you sure you want to delete this ammo?"),
 | 
					            <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
                         qa: "delete-#{ammo_group.id}"
 | 
					          </.link>
 | 
				
			||||||
                       ] do %>
 | 
					
 | 
				
			||||||
 | 
					          <.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>
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
                  <% end %>
 | 
					          </.link>
 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </tbody>
 | 
					 | 
				
			||||||
      </table>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </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}
 | 
				
			||||||
@@ -170,4 +198,5 @@
 | 
				
			|||||||
      />
 | 
					      />
 | 
				
			||||||
    </.modal>
 | 
					    </.modal>
 | 
				
			||||||
  <% true -> %>
 | 
					  <% true -> %>
 | 
				
			||||||
 | 
					    <%= nil %>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,52 +5,153 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use CanneryWeb, :live_view
 | 
					  use CanneryWeb, :live_view
 | 
				
			||||||
  import CanneryWeb.Components.ContainerCard
 | 
					  import CanneryWeb.Components.ContainerCard
 | 
				
			||||||
  alias Cannery.{Ammo, Repo}
 | 
					  alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
 | 
				
			||||||
  alias CanneryWeb.Endpoint
 | 
					  alias CanneryWeb.Endpoint
 | 
				
			||||||
 | 
					  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(
 | 
				
			||||||
        %{"id" => id},
 | 
					        %{"id" => id, "shot_group_id" => shot_group_id},
 | 
				
			||||||
        _url,
 | 
					        _url,
 | 
				
			||||||
        %{assigns: %{live_action: live_action, current_user: current_user}} = socket
 | 
					        %{assigns: %{live_action: live_action, current_user: current_user}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    ammo_group = Ammo.get_ammo_group!(id, current_user) |> Repo.preload([:container, :ammo_type])
 | 
					    shot_group = ActivityLog.get_shot_group!(shot_group_id, current_user)
 | 
				
			||||||
    {:noreply, socket |> assign(page_title: page_title(live_action), ammo_group: ammo_group)}
 | 
					
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(page_title: page_title(live_action), shot_group: shot_group)
 | 
				
			||||||
 | 
					      |> display_ammo_group(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_params(%{"id" => id}, _url, %{assigns: %{live_action: live_action}} = socket) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      socket
 | 
				
			||||||
 | 
					      |> assign(page_title: page_title(live_action))
 | 
				
			||||||
 | 
					      |> display_ammo_group(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp page_title(:add_shot_group), do: gettext("Record Shots")
 | 
				
			||||||
 | 
					  defp page_title(:edit_shot_group), do: gettext("Edit Shot Records")
 | 
				
			||||||
 | 
					  defp page_title(:move), do: gettext("Move Ammo")
 | 
				
			||||||
 | 
					  defp page_title(:show), do: gettext("Show Ammo")
 | 
				
			||||||
 | 
					  defp page_title(:edit), do: gettext("Edit Ammo")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "delete",
 | 
					        "delete",
 | 
				
			||||||
        _,
 | 
					        _params,
 | 
				
			||||||
        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
					        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
				
			||||||
      ) 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
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "toggle_staged",
 | 
					        "toggle_staged",
 | 
				
			||||||
        _,
 | 
					        _params,
 | 
				
			||||||
        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
					        %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {:noreply, socket |> assign(ammo_group: ammo_group)}
 | 
					    {:noreply, socket |> display_ammo_group(ammo_group)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp page_title(:add_shot_group), do: gettext("Add Shot group")
 | 
					  @impl true
 | 
				
			||||||
  defp page_title(:move), do: gettext("Move Ammo group")
 | 
					  def handle_event(
 | 
				
			||||||
  defp page_title(:show), do: gettext("Show Ammo group")
 | 
					        "delete_shot_group",
 | 
				
			||||||
  defp page_title(:edit), do: gettext("Edit Ammo group")
 | 
					        %{"id" => id},
 | 
				
			||||||
 | 
					        %{assigns: %{ammo_group: %{id: ammo_group_id}, current_user: current_user}} = socket
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    {:ok, _} =
 | 
				
			||||||
 | 
					      ActivityLog.get_shot_group!(id, current_user)
 | 
				
			||||||
 | 
					      |> ActivityLog.delete_shot_group(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prompt = dgettext("prompts", "Shot records deleted succesfully")
 | 
				
			||||||
 | 
					    {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_group(ammo_group_id)}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t()
 | 
				
			||||||
 | 
					  defp display_ammo_group(socket, %AmmoGroup{} = ammo_group) do
 | 
				
			||||||
 | 
					    ammo_group = ammo_group |> Repo.preload([:container, :ammo_type, :shot_groups], force: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    columns = [
 | 
				
			||||||
 | 
					      %{label: gettext("Rounds shot"), key: :count},
 | 
				
			||||||
 | 
					      %{label: gettext("Notes"), key: :notes},
 | 
				
			||||||
 | 
					      %{label: gettext("Date"), key: :date},
 | 
				
			||||||
 | 
					      %{label: nil, key: :actions, sortable: false}
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows =
 | 
				
			||||||
 | 
					      ammo_group.shot_groups
 | 
				
			||||||
 | 
					      |> Enum.map(fn shot_group ->
 | 
				
			||||||
 | 
					        ammo_group |> get_table_row_for_shot_group(shot_group, columns)
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(ammo_group: ammo_group, columns: columns, rows: rows)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id),
 | 
				
			||||||
 | 
					    do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec 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
 | 
				
			||||||
 | 
					    assigns = %{ammo_group: ammo_group, shot_group: shot_group}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    columns
 | 
				
			||||||
 | 
					    |> Map.new(fn %{key: key} ->
 | 
				
			||||||
 | 
					      value =
 | 
				
			||||||
 | 
					        case key do
 | 
				
			||||||
 | 
					          :date ->
 | 
				
			||||||
 | 
					            assigns = %{date: date}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {date,
 | 
				
			||||||
 | 
					             ~H"""
 | 
				
			||||||
 | 
					             <.date date={@date} />
 | 
				
			||||||
 | 
					             """}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          :actions ->
 | 
				
			||||||
 | 
					            ~H"""
 | 
				
			||||||
 | 
					            <div class="px-4 py-2 space-x-4 flex justify-center items-center">
 | 
				
			||||||
 | 
					              <.link
 | 
				
			||||||
 | 
					                patch={Routes.ammo_group_show_path(Endpoint, :edit_shot_group, @ammo_group, @shot_group)}
 | 
				
			||||||
 | 
					                class="text-primary-600 link"
 | 
				
			||||||
 | 
					                data-qa={"edit-#{@shot_group.id}"}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <.link
 | 
				
			||||||
 | 
					                href="#"
 | 
				
			||||||
 | 
					                class="text-primary-600 link"
 | 
				
			||||||
 | 
					                phx-click="delete_shot_group"
 | 
				
			||||||
 | 
					                phx-value-id={@shot_group.id}
 | 
				
			||||||
 | 
					                data-confirm={dgettext("prompts", "Are you sure you want to delete this shot record?")}
 | 
				
			||||||
 | 
					                data-qa={"delete-#{@shot_group.id}"}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
 | 
					              </.link>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          key ->
 | 
				
			||||||
 | 
					            shot_group |> Map.get(key)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {key, value}
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,16 @@
 | 
				
			|||||||
      <%= @ammo_group.count %>
 | 
					      <%= @ammo_group.count %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Original count:") %>
 | 
				
			||||||
 | 
					      <%= Ammo.get_original_count(@ammo_group) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Percentage left:") %>
 | 
				
			||||||
 | 
					      <%= gettext("%{percentage}%", percentage: @ammo_group |> Ammo.get_percentage_remaining()) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= 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:") %>
 | 
				
			||||||
@@ -16,58 +26,80 @@
 | 
				
			|||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					      <%= gettext("Purchased on:") %>
 | 
				
			||||||
 | 
					      <.date date={@ammo_group.purchased_on} />
 | 
				
			||||||
 | 
					    </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("Price paid:") %>
 | 
					        <%= gettext("Original cost:") %>
 | 
				
			||||||
        <%= gettext("$%{amount}",
 | 
					        <%= gettext("$%{amount}",
 | 
				
			||||||
          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("Current value:") %>
 | 
				
			||||||
 | 
					        <%= gettext("$%{amount}",
 | 
				
			||||||
 | 
					          amount:
 | 
				
			||||||
 | 
					            (@ammo_group.price_paid * Ammo.get_percentage_remaining(@ammo_group) / 100)
 | 
				
			||||||
 | 
					            |> :erlang.float_to_binary(decimals: 2)
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,9 +113,24 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <.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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= unless @ammo_group.shot_groups |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					    <hr class="mb-4 w-full" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl">
 | 
				
			||||||
 | 
					      <%= gettext("Rounds used") %>
 | 
				
			||||||
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.live_component
 | 
				
			||||||
 | 
					      module={CanneryWeb.Components.TableComponent}
 | 
				
			||||||
 | 
					      id="ammo_group_shot_groups_table"
 | 
				
			||||||
 | 
					      columns={@columns}
 | 
				
			||||||
 | 
					      rows={@rows}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= case @live_action do %>
 | 
					<%= case @live_action do %>
 | 
				
			||||||
@@ -99,6 +146,18 @@
 | 
				
			|||||||
        current_user={@current_user}
 | 
					        current_user={@current_user}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </.modal>
 | 
					    </.modal>
 | 
				
			||||||
 | 
					  <% :edit_shot_group -> %>
 | 
				
			||||||
 | 
					    <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}>
 | 
				
			||||||
 | 
					      <.live_component
 | 
				
			||||||
 | 
					        module={CanneryWeb.RangeLive.FormComponent}
 | 
				
			||||||
 | 
					        id={@shot_group.id}
 | 
				
			||||||
 | 
					        title={@page_title}
 | 
				
			||||||
 | 
					        action={@live_action}
 | 
				
			||||||
 | 
					        shot_group={@shot_group}
 | 
				
			||||||
 | 
					        return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}
 | 
				
			||||||
 | 
					        current_user={@current_user}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </.modal>
 | 
				
			||||||
  <% :add_shot_group -> %>
 | 
					  <% :add_shot_group -> %>
 | 
				
			||||||
    <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}>
 | 
					    <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}>
 | 
				
			||||||
      <.live_component
 | 
					      <.live_component
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,13 +3,13 @@
 | 
				
			|||||||
    <%= @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}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= if @changeset.action && not @changeset.valid? do %>
 | 
					    <%= if @changeset.action && not @changeset.valid? do %>
 | 
				
			||||||
      <div class="invalid-feedback col-span-3 text-center">
 | 
					      <div class="invalid-feedback col-span-3 text-center">
 | 
				
			||||||
@@ -17,11 +17,11 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :name, gettext("Name"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= text_input(f, :name, class: "text-center col-span-2 input input-primary") %>
 | 
					    <%= text_input(f, :name, class: "text-center col-span-2 input input-primary") %>
 | 
				
			||||||
    <%= error_tag(f, :name, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :name, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :desc, gettext("Description"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :desc, gettext("Description"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= textarea(f, :desc,
 | 
					    <%= textarea(f, :desc,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      phx_hook: "MaintainAttrs"
 | 
					      phx_hook: "MaintainAttrs"
 | 
				
			||||||
@@ -34,50 +34,42 @@
 | 
				
			|||||||
    >
 | 
					    >
 | 
				
			||||||
      <%= gettext("Example bullet type abbreviations") %>
 | 
					      <%= gettext("Example bullet type abbreviations") %>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
    <%= label(f, :bullet_type, gettext("Bullet type"),
 | 
					    <%= label(f, :bullet_type, gettext("Bullet type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :bullet_type,
 | 
					    <%= text_input(f, :bullet_type,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: gettext("FMJ")
 | 
					      placeholder: gettext("FMJ")
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :bullet_type, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :bullet_core, gettext("Bullet core"),
 | 
					    <%= label(f, :bullet_core, gettext("Bullet core"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :bullet_core,
 | 
					    <%= text_input(f, :bullet_core,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: gettext("Steel")
 | 
					      placeholder: gettext("Steel")
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :bullet_core, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :bullet_core, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :cartridge, gettext("Cartridge"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :cartridge, gettext("Cartridge"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= text_input(f, :cartridge,
 | 
					    <%= text_input(f, :cartridge,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: "5.56x46mm NATO"
 | 
					      placeholder: "5.56x46mm NATO"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :cartridge, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :cartridge, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :caliber, gettext("Caliber"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :caliber, gettext("Caliber"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= text_input(f, :caliber,
 | 
					    <%= text_input(f, :caliber,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: ".223"
 | 
					      placeholder: ".223"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :caliber, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :caliber, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :case_material, gettext("Case material"),
 | 
					    <%= label(f, :case_material, gettext("Case material"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :case_material,
 | 
					    <%= text_input(f, :case_material,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: gettext("Brass")
 | 
					      placeholder: gettext("Brass")
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :case_material, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :case_material, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :jacket_type, gettext("Jacket type"),
 | 
					    <%= label(f, :jacket_type, gettext("Jacket type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :jacket_type,
 | 
					    <%= text_input(f, :jacket_type,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: gettext("Bimetal")
 | 
					      placeholder: gettext("Bimetal")
 | 
				
			||||||
@@ -85,7 +77,7 @@
 | 
				
			|||||||
    <%= error_tag(f, :case_material, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :case_material, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :muzzle_velocity, gettext("Muzzle velocity"),
 | 
					    <%= label(f, :muzzle_velocity, gettext("Muzzle velocity"),
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					      class: "title text-lg text-primary-600"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= number_input(f, :muzzle_velocity,
 | 
					    <%= number_input(f, :muzzle_velocity,
 | 
				
			||||||
      step: "1",
 | 
					      step: "1",
 | 
				
			||||||
@@ -94,14 +86,12 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :muzzle_velocity, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :powder_type, gettext("Powder type"),
 | 
					    <%= label(f, :powder_type, gettext("Powder type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :powder_type, class: "text-center col-span-2 input input-primary") %>
 | 
					    <%= text_input(f, :powder_type, class: "text-center col-span-2 input input-primary") %>
 | 
				
			||||||
    <%= error_tag(f, :powder_type, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :powder_type, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :powder_grains_per_charge, gettext("Powder grains per charge"),
 | 
					    <%= label(f, :powder_grains_per_charge, gettext("Powder grains per charge"),
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					      class: "title text-lg text-primary-600"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= number_input(f, :powder_grains_per_charge,
 | 
					    <%= number_input(f, :powder_grains_per_charge,
 | 
				
			||||||
      step: "1",
 | 
					      step: "1",
 | 
				
			||||||
@@ -110,7 +100,7 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :powder_grains_per_charge, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :grains, gettext("Grains"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :grains, gettext("Grains"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= number_input(f, :grains,
 | 
					    <%= number_input(f, :grains,
 | 
				
			||||||
      step: "1",
 | 
					      step: "1",
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
@@ -118,54 +108,48 @@
 | 
				
			|||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :grains, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :grains, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :pressure, gettext("Pressure"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :pressure, gettext("Pressure"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= text_input(f, :pressure,
 | 
					    <%= text_input(f, :pressure,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: "+P"
 | 
					      placeholder: "+P"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :pressure, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :pressure, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :primer_type, gettext("Primer type"),
 | 
					    <%= label(f, :primer_type, gettext("Primer type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :primer_type,
 | 
					    <%= text_input(f, :primer_type,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: "Boxer"
 | 
					      placeholder: "Boxer"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :primer_type, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :primer_type, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :firing_type, gettext("Firing type"),
 | 
					    <%= label(f, :firing_type, gettext("Firing type"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :firing_type,
 | 
					    <%= text_input(f, :firing_type,
 | 
				
			||||||
      class: "text-center col-span-2 input input-primary",
 | 
					      class: "text-center col-span-2 input input-primary",
 | 
				
			||||||
      placeholder: "Centerfire"
 | 
					      placeholder: "Centerfire"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= error_tag(f, :firing_type, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :firing_type, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :tracer, gettext("Tracer"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :tracer, gettext("Tracer"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= checkbox(f, :tracer, class: "text-center col-span-2 checkbox") %>
 | 
					    <%= checkbox(f, :tracer, class: "text-center col-span-2 checkbox") %>
 | 
				
			||||||
    <%= error_tag(f, :tracer, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :tracer, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :incendiary, gettext("Incendiary"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :incendiary, gettext("Incendiary"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= checkbox(f, :incendiary, class: "text-center col-span-2 checkbox") %>
 | 
					    <%= checkbox(f, :incendiary, class: "text-center col-span-2 checkbox") %>
 | 
				
			||||||
    <%= error_tag(f, :incendiary, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :incendiary, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :blank, gettext("Blank"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :blank, gettext("Blank"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= checkbox(f, :blank, class: "text-center col-span-2 checkbox") %>
 | 
					    <%= checkbox(f, :blank, class: "text-center col-span-2 checkbox") %>
 | 
				
			||||||
    <%= error_tag(f, :blank, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :blank, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :corrosive, gettext("Corrosive"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :corrosive, gettext("Corrosive"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= checkbox(f, :corrosive, class: "text-center col-span-2 checkbox") %>
 | 
					    <%= checkbox(f, :corrosive, class: "text-center col-span-2 checkbox") %>
 | 
				
			||||||
    <%= error_tag(f, :corrosive, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :corrosive, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :manufacturer, gettext("Manufacturer"),
 | 
					    <%= label(f, :manufacturer, gettext("Manufacturer"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
      class: "mr-4 title text-lg text-primary-600"
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
    <%= text_input(f, :manufacturer, class: "text-center col-span-2 input input-primary") %>
 | 
					    <%= text_input(f, :manufacturer, class: "text-center col-span-2 input input-primary") %>
 | 
				
			||||||
    <%= error_tag(f, :manufacturer, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :manufacturer, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :upc, gettext("UPC"), class: "mr-4 title text-lg text-primary-600") %>
 | 
					    <%= label(f, :upc, gettext("UPC"), class: "title text-lg text-primary-600") %>
 | 
				
			||||||
    <%= text_input(f, :upc, class: "text-center col-span-2 input input-primary") %>
 | 
					    <%= text_input(f, :upc, class: "text-center col-span-2 input input-primary") %>
 | 
				
			||||||
    <%= error_tag(f, :upc, "col-span-3 text-center") %>
 | 
					    <%= error_tag(f, :upc, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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("Listing 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,37 +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()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns_to_display =
 | 
					  @impl true
 | 
				
			||||||
      [
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
        {gettext("Name"), :name, :string},
 | 
					    {:noreply, socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :index))}
 | 
				
			||||||
        {gettext("Bullet type"), :bullet_type, :string},
 | 
					  end
 | 
				
			||||||
        {gettext("Bullet core"), :bullet_core, :string},
 | 
					 | 
				
			||||||
        {gettext("Cartridge"), :cartridge, :string},
 | 
					 | 
				
			||||||
        {gettext("Caliber"), :caliber, :string},
 | 
					 | 
				
			||||||
        {gettext("Case material"), :case_material, :string},
 | 
					 | 
				
			||||||
        {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("Rimfire"), :rimfire, :boolean},
 | 
					 | 
				
			||||||
        {gettext("Tracer"), :tracer, :boolean},
 | 
					 | 
				
			||||||
        {gettext("Incendiary"), :incendiary, :boolean},
 | 
					 | 
				
			||||||
        {gettext("Blank"), :blank, :boolean},
 | 
					 | 
				
			||||||
        {gettext("Corrosive"), :corrosive, :boolean},
 | 
					 | 
				
			||||||
        {gettext("Manufacturer"), :manufacturer, :string},
 | 
					 | 
				
			||||||
        {gettext("UPC"), :upc, :string}
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
      # filter columns to only used ones
 | 
					 | 
				
			||||||
      |> Enum.filter(fn {_label, field, _type} ->
 | 
					 | 
				
			||||||
        ammo_types |> Enum.any?(fn ammo_type -> not (ammo_type |> Map.get(field) |> is_nil()) end)
 | 
					 | 
				
			||||||
      end)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket |> assign(ammo_types: ammo_types, columns_to_display: columns_to_display)
 | 
					  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply,
 | 
				
			||||||
 | 
					     socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :search, search_term))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp list_ammo_types(%{assigns: %{search: search, current_user: current_user}} = socket) do
 | 
				
			||||||
 | 
					    socket |> assign(ammo_types: Ammo.list_ammo_types(search, current_user))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,86 +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 @ammo_types |> 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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <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_type_search"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					          class: "input input-primary",
 | 
				
			||||||
 | 
					          value: @search,
 | 
				
			||||||
 | 
					          phx_debounce: 300,
 | 
				
			||||||
 | 
					          placeholder: gettext("Search catalog")
 | 
				
			||||||
        ) %>
 | 
					        ) %>
 | 
				
			||||||
 | 
					      </.form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
 | 
					      <.toggle_button action="toggle_show_used" value={@show_used}>
 | 
				
			||||||
      <table class="min-w-full table-auto text-center bg-white">
 | 
					        <span class="title text-lg text-primary-600">
 | 
				
			||||||
        <thead class="border-b border-primary-600">
 | 
					          <%= gettext("Show used") %>
 | 
				
			||||||
          <tr>
 | 
					        </span>
 | 
				
			||||||
            <%= for {field_name, _field, _type} <- @columns_to_display do %>
 | 
					      </.toggle_button>
 | 
				
			||||||
              <th class="p-2">
 | 
					    </div>
 | 
				
			||||||
                <%= field_name %>
 | 
					 | 
				
			||||||
              </th>
 | 
					 | 
				
			||||||
            <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <th class="p-2"></th>
 | 
					    <%= if @ammo_types |> Enum.empty?() do %>
 | 
				
			||||||
          </tr>
 | 
					      <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
        </thead>
 | 
					        <%= gettext("No Ammo types") %>
 | 
				
			||||||
        <tbody>
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
          <%= for ammo_type <- @ammo_types do %>
 | 
					      </h2>
 | 
				
			||||||
            <tr id={"ammo_type-#{ammo_type.id}"}>
 | 
					    <% else %>
 | 
				
			||||||
              <%= for {_label, field, type} <- @columns_to_display do %>
 | 
					      <.live_component
 | 
				
			||||||
                <td class="p-2">
 | 
					        module={CanneryWeb.Components.AmmoTypeTableComponent}
 | 
				
			||||||
                  <%= case type do %>
 | 
					        id="ammo_types_index_table"
 | 
				
			||||||
                    <% :boolean -> %>
 | 
					        action={@live_action}
 | 
				
			||||||
                      <%= ammo_type |> Map.get(field) |> humanize() %>
 | 
					        ammo_types={@ammo_types}
 | 
				
			||||||
                    <% _other -> %>
 | 
					        current_user={@current_user}
 | 
				
			||||||
                      <%= ammo_type |> Map.get(field) %>
 | 
					        show_used={@show_used}
 | 
				
			||||||
                  <% end %>
 | 
					      >
 | 
				
			||||||
                </td>
 | 
					        <:actions :let={ammo_type}>
 | 
				
			||||||
              <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
          <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_redirect to: Routes.ammo_type_show_path(Endpoint, :show, ammo_type),
 | 
					            <.link
 | 
				
			||||||
                                class: "text-primary-600 link",
 | 
					              navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)}
 | 
				
			||||||
                                data: [qa: "view-#{ammo_type.id}"] do %>
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"view-#{ammo_type.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
					              <i class="fa-fw fa-lg fas fa-eye"></i>
 | 
				
			||||||
                  <% end %>
 | 
					            </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <%= live_patch to: Routes.ammo_type_index_path(Endpoint, :edit, ammo_type),
 | 
					            <.link
 | 
				
			||||||
                             class: "text-primary-600 link",
 | 
					              patch={Routes.ammo_type_index_path(Endpoint, :edit, ammo_type)}
 | 
				
			||||||
                             data: [qa: "edit-#{ammo_type.id}"] do %>
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					              data-qa={"edit-#{ammo_type.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",
 | 
					              patch={Routes.ammo_type_index_path(Endpoint, :clone, ammo_type)}
 | 
				
			||||||
                       phx_click: "delete",
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
                       phx_value_id: ammo_type.id,
 | 
					              data-qa={"clone-#{ammo_type.id}"}
 | 
				
			||||||
                       data: [
 | 
					            >
 | 
				
			||||||
                         confirm: dgettext("prompts", "Are you sure you want to delete this ammo?"),
 | 
					              <i class="fa-fw fa-lg fas fa-copy"></i>
 | 
				
			||||||
                         qa: "delete-#{ammo_type.id}"
 | 
					            </.link>
 | 
				
			||||||
                       ] do %>
 | 
					
 | 
				
			||||||
 | 
					            <.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>
 | 
					              <i class="fa-lg fas fa-trash"></i>
 | 
				
			||||||
                  <% end %>
 | 
					            </.link>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
              </td>
 | 
					        </:actions>
 | 
				
			||||||
            </tr>
 | 
					      </.live_component>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
        </tbody>
 | 
					 | 
				
			||||||
      </table>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  <% 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}, _, %{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
 | 
				
			||||||
@@ -32,7 +47,7 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
 | 
				
			|||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "delete",
 | 
					        "delete",
 | 
				
			||||||
        _,
 | 
					        _params,
 | 
				
			||||||
        %{assigns: %{ammo_type: ammo_type, current_user: current_user}} = socket
 | 
					        %{assigns: %{ammo_type: ammo_type, current_user: current_user}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    %{name: ammo_type_name} = ammo_type |> Ammo.delete_ammo_type!(current_user)
 | 
					    %{name: ammo_type_name} = ammo_type |> Ammo.delete_ammo_type!(current_user)
 | 
				
			||||||
@@ -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,115 +1,194 @@
 | 
				
			|||||||
<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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <%= if @ammo_type.desc do %>
 | 
					  <%= if @ammo_type.desc do %>
 | 
				
			||||||
    <span
 | 
					    <span class="max-w-2xl w-full px-8 py-4 rounded-lg
 | 
				
			||||||
      class="max-w-2xl w-full px-8 py-4 rounded-lg
 | 
					 | 
				
			||||||
      text-center title text-lg
 | 
					      text-center title text-lg
 | 
				
			||||||
      border border-primary-600"
 | 
					      border border-primary-600">
 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <%= @ammo_type.desc %>
 | 
					      <%= @ammo_type.desc %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
  <% 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 text-center justify-center items-center">
 | 
					  <%= if @fields_to_display do %>
 | 
				
			||||||
    <%= for {field_name, field} <- [
 | 
					    <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
 | 
				
			||||||
          {gettext("Bullet type"), :bullet_type},
 | 
					      <%= for %{label: label, key: key, type: type} <- @fields_list do %>
 | 
				
			||||||
          {gettext("Bullet core"), :bullet_core},
 | 
					        <%= if @ammo_type |> Map.get(key) do %>
 | 
				
			||||||
          {gettext("Cartridge"), :cartridge},
 | 
					          <h3 class="title text-lg">
 | 
				
			||||||
          {gettext("Caliber"), :caliber},
 | 
					            <%= label %>
 | 
				
			||||||
          {gettext("Case material"), :case_material},
 | 
					 | 
				
			||||||
          {gettext("Jacket type"), :jacket_type},
 | 
					 | 
				
			||||||
          {gettext("Muzzle velocity"), :muzzle_velocity},
 | 
					 | 
				
			||||||
          {gettext("Powder type"), :powder_type},
 | 
					 | 
				
			||||||
          {gettext("Powder grains per charge"), :powder_grains_per_charge},
 | 
					 | 
				
			||||||
          {gettext("Grains"), :grains},
 | 
					 | 
				
			||||||
          {gettext("Pressure"), :pressure},
 | 
					 | 
				
			||||||
          {gettext("Primer type"), :primer_type}
 | 
					 | 
				
			||||||
        ] do %>
 | 
					 | 
				
			||||||
      <%= if @ammo_type |> Map.get(field) do %>
 | 
					 | 
				
			||||||
        <h3 class="mb-2 sm:mr-4 title text-lg">
 | 
					 | 
				
			||||||
          <%= field_name %>:
 | 
					 | 
				
			||||||
          </h3>
 | 
					          </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span class="mb-4 sm:mb-2 text-primary-600">
 | 
					          <span class="text-primary-600">
 | 
				
			||||||
          <%= @ammo_type |> Map.get(field) %>
 | 
					            <%= case type do %>
 | 
				
			||||||
 | 
					              <% :boolean -> %>
 | 
				
			||||||
 | 
					                <%= @ammo_type |> Map.get(key) |> humanize() %>
 | 
				
			||||||
 | 
					              <% _ -> %>
 | 
				
			||||||
 | 
					                <%= @ammo_type |> Map.get(key) %>
 | 
				
			||||||
 | 
					            <% end %>
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= for {field_name, field} <- [
 | 
					    <hr class="hr" />
 | 
				
			||||||
          {"Rimfire", :rimfire},
 | 
					  <% end %>
 | 
				
			||||||
          {"Tracer", :tracer},
 | 
					
 | 
				
			||||||
          {"Incendiary", :incendiary},
 | 
					  <div class="grid sm:grid-cols-2 gap-4 text-center justify-center items-center">
 | 
				
			||||||
          {"Blank", :blank},
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
          {"Corrosive", :corrosive}
 | 
					      <%= gettext("Rounds:") %>
 | 
				
			||||||
        ] do %>
 | 
					 | 
				
			||||||
      <h3 class="mb-2 sm:mr-4 title text-lg">
 | 
					 | 
				
			||||||
        <%= field_name %>:
 | 
					 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <span class="mb-4 sm:mb-2 text-primary-600">
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
        <%= @ammo_type |> Map.get(field) |> humanize() %>
 | 
					      <%= @ammo_type |> Ammo.get_round_count_for_ammo_type(@current_user) %>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= for {field_name, field} <- [{"Manufacturer", :manufacturer}, {"UPC", :upc}] do %>
 | 
					    <h3 class="title text-lg">
 | 
				
			||||||
      <%= if @ammo_type |> Map.get(field) do %>
 | 
					      <%= gettext("Used rounds:") %>
 | 
				
			||||||
        <h3 class="mb-2 sm:mr-4 title text-lg">
 | 
					 | 
				
			||||||
          <%= field_name %>:
 | 
					 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span class="mb-4 sm:mb-2 text-primary-600">
 | 
					    <span class="text-primary-600">
 | 
				
			||||||
          <%= @ammo_type |> Map.get(field) %>
 | 
					      <%= @ammo_type |> Ammo.get_used_count_for_ammo_type(@current_user) %>
 | 
				
			||||||
 | 
					    </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">
 | 
				
			||||||
 | 
					      <.datetime datetime={@ammo_type.inserted_at} />
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @avg_cost_per_round do %>
 | 
					    <%= if @avg_cost_per_round do %>
 | 
				
			||||||
      <h3 class="mb-2 sm:mr-4 title text-lg">
 | 
					      <h3 class="title text-lg">
 | 
				
			||||||
        <%= gettext("Average Price paid") %>:
 | 
					        <%= gettext("Average CPR") %>:
 | 
				
			||||||
      </h3>
 | 
					      </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <span class="mb-4 sm:mb-2 text-primary-600">
 | 
					      <span class="text-primary-600">
 | 
				
			||||||
        <%= gettext("$%{amount}",
 | 
					        <%= gettext("$%{amount}",
 | 
				
			||||||
          amount: @avg_cost_per_round |> :erlang.float_to_binary(decimals: 2)
 | 
					          amount: @avg_cost_per_round |> :erlang.float_to_binary(decimals: 2)
 | 
				
			||||||
        ) %>
 | 
					        ) %>
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <h3 class="mx-8 my-4 title text-lg text-primary-600 col-span-2">
 | 
				
			||||||
 | 
					        <%= gettext("No cost information") %>
 | 
				
			||||||
 | 
					      </h3>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <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="px-4 title text-lg text-primary-600">
 | 
				
			||||||
        <%= gettext("No ammo for this type") %>
 | 
					        <%= gettext("No ammo for this type") %>
 | 
				
			||||||
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
    <% else %>
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <%= if @view_table do %>
 | 
				
			||||||
 | 
					        <.live_component
 | 
				
			||||||
 | 
					          module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
 | 
					          id="ammo-type-show-table"
 | 
				
			||||||
 | 
					          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 %>
 | 
					          <%= for ammo_group <- @ammo_groups do %>
 | 
				
			||||||
        <.ammo_group_card ammo_group={ammo_group} />
 | 
					            <.ammo_group_card ammo_group={ammo_group} show_container={true} />
 | 
				
			||||||
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,51 +0,0 @@
 | 
				
			|||||||
defmodule CanneryWeb.ContainerLive.AddTagComponent do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Livecomponent that can add a tag to a Container
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  use CanneryWeb, :live_component
 | 
					 | 
				
			||||||
  alias Cannery.{Accounts.User, Containers, Containers.Container, Tags, Tags.Tag}
 | 
					 | 
				
			||||||
  alias Phoenix.LiveView.Socket
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @impl true
 | 
					 | 
				
			||||||
  @spec update(
 | 
					 | 
				
			||||||
          %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
 | 
					 | 
				
			||||||
          Socket.t()
 | 
					 | 
				
			||||||
        ) :: {:ok, Socket.t()}
 | 
					 | 
				
			||||||
  def update(%{container: _container, current_user: current_user} = assigns, socket) do
 | 
					 | 
				
			||||||
    {:ok, socket |> assign(assigns) |> assign(:tags, Tags.list_tags(current_user))}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @impl true
 | 
					 | 
				
			||||||
  def handle_event(
 | 
					 | 
				
			||||||
        "save",
 | 
					 | 
				
			||||||
        %{"tag" => %{"tag_id" => tag_id}},
 | 
					 | 
				
			||||||
        %{
 | 
					 | 
				
			||||||
          assigns: %{
 | 
					 | 
				
			||||||
            tags: tags,
 | 
					 | 
				
			||||||
            container: container,
 | 
					 | 
				
			||||||
            current_user: current_user,
 | 
					 | 
				
			||||||
            return_to: return_to
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } = socket
 | 
					 | 
				
			||||||
      ) do
 | 
					 | 
				
			||||||
    socket =
 | 
					 | 
				
			||||||
      case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
 | 
					 | 
				
			||||||
        nil ->
 | 
					 | 
				
			||||||
          prompt = dgettext("errors", "Tag could not be added")
 | 
					 | 
				
			||||||
          socket |> put_flash(:error, prompt)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        %{name: tag_name} = tag ->
 | 
					 | 
				
			||||||
          _container_tag = Containers.add_tag!(container, tag, current_user)
 | 
					 | 
				
			||||||
          prompt = dgettext("prompts", "%{name} added successfully", name: tag_name)
 | 
					 | 
				
			||||||
          socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {:noreply, socket}
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @spec tag_options([Tag.t()]) :: [{String.t(), Tag.id()}]
 | 
					 | 
				
			||||||
  defp tag_options(tags) do
 | 
					 | 
				
			||||||
    tags |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
<div>
 | 
					 | 
				
			||||||
  <h2 class="mb-8 text-center title text-xl text-primary-600">
 | 
					 | 
				
			||||||
    <%= @title %>
 | 
					 | 
				
			||||||
  </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <.form
 | 
					 | 
				
			||||||
    let={f}
 | 
					 | 
				
			||||||
    for={:tag}
 | 
					 | 
				
			||||||
    id="add-tag-to-container-form"
 | 
					 | 
				
			||||||
    class="flex flex-col sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					 | 
				
			||||||
    phx-target={@myself}
 | 
					 | 
				
			||||||
    phx-submit="save"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <%= select(f, :tag_id, tag_options(@tags), class: "text-center col-span-2 input input-primary") %>
 | 
					 | 
				
			||||||
    <%= error_tag(f, :tag_id, "col-span-3 text-center") %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= submit(dgettext("actions", "Add"),
 | 
					 | 
				
			||||||
      class: "mx-auto btn btn-primary",
 | 
					 | 
				
			||||||
      phx_disable_with: dgettext("prompts", "Adding...")
 | 
					 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
  </.form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
							
								
								
									
										73
									
								
								lib/cannery_web/live/container_live/edit_tags_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/cannery_web/live/container_live/edit_tags_component.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					defmodule CanneryWeb.ContainerLive.EditTagsComponent do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Livecomponent that can add or remove a tag to a Container
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  use CanneryWeb, :live_component
 | 
				
			||||||
 | 
					  alias Cannery.{Accounts.User, Containers, Containers.Container, Repo, Tags, Tags.Tag}
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.Socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  @spec update(
 | 
				
			||||||
 | 
					          %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
 | 
				
			||||||
 | 
					          Socket.t()
 | 
				
			||||||
 | 
					        ) :: {:ok, Socket.t()}
 | 
				
			||||||
 | 
					  def update(%{container: container, current_user: current_user} = assigns, socket) do
 | 
				
			||||||
 | 
					    tags = Tags.list_tags(current_user)
 | 
				
			||||||
 | 
					    container = container |> Repo.preload(:tags)
 | 
				
			||||||
 | 
					    {:ok, socket |> assign(assigns) |> assign(tags: tags, container: container)}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event(
 | 
				
			||||||
 | 
					        "save",
 | 
				
			||||||
 | 
					        %{"tag" => %{"tag_id" => tag_id}},
 | 
				
			||||||
 | 
					        %{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
 | 
				
			||||||
 | 
					        nil ->
 | 
				
			||||||
 | 
					          prompt = dgettext("errors", "Tag could not be added")
 | 
				
			||||||
 | 
					          socket |> put_flash(:error, prompt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        %{name: tag_name} = tag ->
 | 
				
			||||||
 | 
					          _container_tag = Containers.add_tag!(container, tag, current_user)
 | 
				
			||||||
 | 
					          container = container |> Repo.preload(:tags, force: true)
 | 
				
			||||||
 | 
					          prompt = dgettext("prompts", "%{name} added successfully", name: tag_name)
 | 
				
			||||||
 | 
					          socket |> put_flash(:info, prompt) |> assign(container: container)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event(
 | 
				
			||||||
 | 
					        "delete",
 | 
				
			||||||
 | 
					        %{"tag-id" => tag_id},
 | 
				
			||||||
 | 
					        %{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
 | 
				
			||||||
 | 
					      ) do
 | 
				
			||||||
 | 
					    socket =
 | 
				
			||||||
 | 
					      case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
 | 
				
			||||||
 | 
					        nil ->
 | 
				
			||||||
 | 
					          prompt = dgettext("errors", "Tag could not be removed")
 | 
				
			||||||
 | 
					          socket |> put_flash(:error, prompt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        %{name: tag_name} = tag ->
 | 
				
			||||||
 | 
					          _container_tag = Containers.remove_tag!(container, tag, current_user)
 | 
				
			||||||
 | 
					          container = container |> Repo.preload(:tags, force: true)
 | 
				
			||||||
 | 
					          prompt = dgettext("prompts", "%{name} removed successfully", name: tag_name)
 | 
				
			||||||
 | 
					          socket |> put_flash(:info, prompt) |> assign(container: container)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {:noreply, socket}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec tag_options([Tag.t()], Container.t()) :: [{String.t(), Tag.id()}]
 | 
				
			||||||
 | 
					  defp tag_options(tags, %Container{tags: container_tags}) do
 | 
				
			||||||
 | 
					    container_tags_map = container_tags |> Enum.map(fn %{id: id} -> id end) |> MapSet.new()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tags
 | 
				
			||||||
 | 
					    |> Enum.reject(fn %{id: id} -> container_tags_map |> MapSet.member?(id) end)
 | 
				
			||||||
 | 
					    |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					<div class="flex flex-col justify-center items-center text-center space-y-8">
 | 
				
			||||||
 | 
					  <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					    <%= @title %>
 | 
				
			||||||
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
 | 
					    <%= for tag <- @container.tags do %>
 | 
				
			||||||
 | 
					      <.link
 | 
				
			||||||
 | 
					        href="#"
 | 
				
			||||||
 | 
					        class="mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
 | 
				
			||||||
 | 
					        style={"color: #{tag.text_color}; background-color: #{tag.bg_color}"}
 | 
				
			||||||
 | 
					        phx-click="delete"
 | 
				
			||||||
 | 
					        phx-value-tag-id={tag.id}
 | 
				
			||||||
 | 
					        phx-target={@myself}
 | 
				
			||||||
 | 
					        data-confirm={
 | 
				
			||||||
 | 
					          dgettext(
 | 
				
			||||||
 | 
					            "prompts",
 | 
				
			||||||
 | 
					            "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
 | 
				
			||||||
 | 
					            tag_name: tag.name,
 | 
				
			||||||
 | 
					            container_name: @container.name
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= tag.name %>
 | 
				
			||||||
 | 
					        <i class="fa-fw fa-sm fas fa-trash"></i>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <%= if @container.tags |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					      <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					        <%= gettext("No tags") %>
 | 
				
			||||||
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= unless tag_options(@tags, @container) |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					    <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <.form
 | 
				
			||||||
 | 
					      :let={f}
 | 
				
			||||||
 | 
					      for={:tag}
 | 
				
			||||||
 | 
					      id="add-tag-to-container-form"
 | 
				
			||||||
 | 
					      class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
 | 
					      phx-target={@myself}
 | 
				
			||||||
 | 
					      phx-submit="save"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= select(f, :tag_id, tag_options(@tags, @container),
 | 
				
			||||||
 | 
					        class: "text-center col-span-2 input input-primary"
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					      <%= error_tag(f, :tag_id, "col-span-3 text-center") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <%= submit(dgettext("actions", "Add"),
 | 
				
			||||||
 | 
					        class: "mx-auto btn btn-primary",
 | 
				
			||||||
 | 
					        phx_disable_with: dgettext("prompts", "Adding...")
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					    </.form>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -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,10 +3,10 @@
 | 
				
			|||||||
    <%= @title %>
 | 
					    <%= @title %>
 | 
				
			||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="container-form"
 | 
					    id="container-form"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,32 +5,69 @@ 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}
 | 
					  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) |> display_containers()}
 | 
					    {: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
 | 
				
			||||||
  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_containers()}
 | 
				
			||||||
  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: container_name} =
 | 
				
			||||||
 | 
					      container =
 | 
				
			||||||
 | 
					      Containers.get_container!(id, current_user)
 | 
				
			||||||
 | 
					      |> Repo.preload([:tags, :ammo_groups])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket
 | 
					    socket
 | 
				
			||||||
    |> assign(:page_title, gettext("Edit Container"))
 | 
					    |> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
 | 
				
			||||||
    |> assign(:container, Containers.get_container!(id, current_user))
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :new, _params) do
 | 
					  defp apply_action(socket, :new, _params) 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 |> assign(:page_title, gettext("Listing Containers")) |> assign(:container, nil)
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      page_title: gettext("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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
 | 
				
			||||||
 | 
					    %{name: container_name} =
 | 
				
			||||||
 | 
					      container =
 | 
				
			||||||
 | 
					      Containers.get_container!(id, current_user) |> Repo.preload([:tags, :ammo_groups])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    page_title = gettext("Edit %{name} tags", name: container_name)
 | 
				
			||||||
 | 
					    socket |> assign(page_title: page_title, container: container)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -69,7 +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
 | 
				
			||||||
    socket |> assign(containers: Containers.list_containers(current_user))
 | 
					  def handle_event("toggle_table", _params, %{assigns: %{view_table: view_table}} = socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> assign(:view_table, !view_table) |> display_containers()}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @impl true
 | 
				
			||||||
 | 
					  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
				
			||||||
 | 
					    {:noreply, socket |> push_patch(to: Routes.container_index_path(Endpoint, :index))}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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,49 +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}
 | 
				
			||||||
 | 
					        current_user={@current_user}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <:tag_actions :let={container}>
 | 
				
			||||||
 | 
					          <div class="mx-4 my-2">
 | 
				
			||||||
 | 
					            <.link
 | 
				
			||||||
 | 
					              patch={Routes.container_index_path(Endpoint, :edit_tags, container)}
 | 
				
			||||||
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
				
			||||||
 | 
					            </.link>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </:tag_actions>
 | 
				
			||||||
 | 
					        <:actions :let={container}>
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            patch={Routes.container_index_path(Endpoint, :edit, container)}
 | 
				
			||||||
 | 
					            class="text-primary-600 link"
 | 
				
			||||||
 | 
					            data-qa={"edit-#{container.id}"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row flex-wrap justify-center items-center">
 | 
					          <.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>
 | 
				
			||||||
 | 
					        </:actions>
 | 
				
			||||||
 | 
					      </.live_component>
 | 
				
			||||||
 | 
					    <% else %>
 | 
				
			||||||
 | 
					      <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
        <%= for container <- @containers do %>
 | 
					        <%= for container <- @containers do %>
 | 
				
			||||||
          <.container_card container={container}>
 | 
					          <.container_card container={container}>
 | 
				
			||||||
        <%= live_patch to: Routes.container_index_path(Endpoint, :edit, container),
 | 
					            <:tag_actions>
 | 
				
			||||||
                   class: "text-primary-600 link",
 | 
					              <div class="mx-4 my-2">
 | 
				
			||||||
                   data: [qa: "edit-#{container.id}"] do %>
 | 
					                <.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>
 | 
					              <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
        <% end %>
 | 
					            </.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
 | 
				
			||||||
 | 
					              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>
 | 
					              <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
        <% end %>
 | 
					            </.link>
 | 
				
			||||||
          </.container_card>
 | 
					          </.container_card>
 | 
				
			||||||
        <% end %>
 | 
					        <% end %>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
 | 
					  <% 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}
 | 
				
			||||||
@@ -58,3 +161,16 @@
 | 
				
			|||||||
    />
 | 
					    />
 | 
				
			||||||
  </.modal>
 | 
					  </.modal>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= if @live_action == :edit_tags do %>
 | 
				
			||||||
 | 
					  <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
 | 
				
			||||||
 | 
					    <.live_component
 | 
				
			||||||
 | 
					      module={CanneryWeb.ContainerLive.EditTagsComponent}
 | 
				
			||||||
 | 
					      id={@container.id}
 | 
				
			||||||
 | 
					      title={@page_title}
 | 
				
			||||||
 | 
					      action={@live_action}
 | 
				
			||||||
 | 
					      container={@container}
 | 
				
			||||||
 | 
					      current_user={@current_user}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </.modal>
 | 
				
			||||||
 | 
					<% end %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,24 +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 =
 | 
				
			||||||
        _,
 | 
					      socket
 | 
				
			||||||
        %{assigns: %{current_user: current_user, live_action: live_action}} = socket
 | 
					      |> assign(view_table: true)
 | 
				
			||||||
      ) do
 | 
					      |> render_container(id, current_user)
 | 
				
			||||||
    {:noreply,
 | 
					
 | 
				
			||||||
     socket |> assign(page_title: page_title(live_action)) |> render_container(id, current_user)}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -42,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)
 | 
				
			||||||
@@ -54,7 +53,7 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "delete_container",
 | 
					        "delete_container",
 | 
				
			||||||
        _,
 | 
					        _params,
 | 
				
			||||||
        %{assigns: %{container: container, current_user: current_user}} = socket
 | 
					        %{assigns: %{container: container, current_user: current_user}} = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
@@ -65,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(", ")
 | 
				
			||||||
@@ -85,16 +84,43 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
				
			|||||||
    {:noreply, socket}
 | 
					    {:noreply, socket}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp page_title(:show), do: gettext("Show Container")
 | 
					  @impl true
 | 
				
			||||||
  defp page_title(:edit), do: gettext("Edit Container")
 | 
					  def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do
 | 
				
			||||||
  defp page_title(:add_tag), do: gettext("Add Tag to Container")
 | 
					    {: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(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} =
 | 
				
			||||||
      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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket |> assign(container: container)
 | 
					    ammo_groups = Ammo.list_ammo_groups_for_container(container, current_user, show_used)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    page_title =
 | 
				
			||||||
 | 
					      case live_action do
 | 
				
			||||||
 | 
					        action when action in [:show, :table] -> container_name
 | 
				
			||||||
 | 
					        :edit -> gettext("Edit %{name}", name: container_name)
 | 
				
			||||||
 | 
					        :edit_tags -> gettext("Edit %{name} tags", name: container_name)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket |> assign(container: container, ammo_groups: ammo_groups, page_title: page_title)
 | 
				
			||||||
 | 
					  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 %>
 | 
				
			||||||
 | 
					        <%= gettext("Packs:") %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					      <%= Enum.count(@ammo_groups) %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span class="rounded-lg title text-lg">
 | 
				
			||||||
 | 
					      <%= if @show_used do %>
 | 
				
			||||||
 | 
					        <%= gettext("Total rounds:") %>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <%= gettext("Rounds:") %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					      <%= @container |> Containers.get_container_rounds!() %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= link to: "#",
 | 
					  <div class="flex space-x-4 justify-center items-center text-primary-600">
 | 
				
			||||||
         class: "text-primary-600 link",
 | 
					    <.link
 | 
				
			||||||
         phx_click: "delete_container",
 | 
					      patch={Routes.container_show_path(Endpoint, :edit, @container)}
 | 
				
			||||||
         data: [
 | 
					      class="text-primary-600 link"
 | 
				
			||||||
           confirm:
 | 
					      data-qa="edit"
 | 
				
			||||||
             dgettext("prompts", "Are you sure you want to delete %{name}?", name: @container.name),
 | 
					    >
 | 
				
			||||||
           qa: "delete"
 | 
					      <i class="fa-fw fa-lg fas fa-edit"></i>
 | 
				
			||||||
         ] do %>
 | 
					    </.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,48 +73,74 @@
 | 
				
			|||||||
        <%= display_emoji("😔") %>
 | 
					        <%= display_emoji("😔") %>
 | 
				
			||||||
      </h2>
 | 
					      </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= live_patch(dgettext("actions", "Why not add one?"),
 | 
					      <.link
 | 
				
			||||||
        to: Routes.container_show_path(Endpoint, :add_tag, @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 %>
 | 
				
			||||||
    <h2 class="mb-4 title text-xl text-primary-600">
 | 
					    <div class="flex flex-wrap justify-center items-center">
 | 
				
			||||||
      <%= gettext("Tags") %>
 | 
					 | 
				
			||||||
    </h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= for tag <- @container.tags do %>
 | 
					      <%= for tag <- @container.tags do %>
 | 
				
			||||||
      <.tag_card tag={tag}>
 | 
					        <.simple_tag_card tag={tag} />
 | 
				
			||||||
        <%= link to: "#",
 | 
					 | 
				
			||||||
             class: "text-primary-600 link",
 | 
					 | 
				
			||||||
             phx_click: "delete_tag",
 | 
					 | 
				
			||||||
             phx_value_tag_id: tag.id,
 | 
					 | 
				
			||||||
             data: [
 | 
					 | 
				
			||||||
               confirm:
 | 
					 | 
				
			||||||
                 dgettext(
 | 
					 | 
				
			||||||
                   "prompts",
 | 
					 | 
				
			||||||
                   "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
 | 
					 | 
				
			||||||
                   tag_name: tag.name,
 | 
					 | 
				
			||||||
                   container_name: @container.name
 | 
					 | 
				
			||||||
                 )
 | 
					 | 
				
			||||||
             ] do %>
 | 
					 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </.tag_card>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="mx-4 my-2">
 | 
				
			||||||
 | 
					        <.link
 | 
				
			||||||
 | 
					          patch={Routes.container_show_path(Endpoint, :edit_tags, @container)}
 | 
				
			||||||
 | 
					          class="text-primary-600 link"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <i class="fa-fw fa-lg fas fa-tags"></i>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <hr class="mb-4 hr" />
 | 
					  <hr class="mb-4 hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <p>
 | 
					  <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}>
 | 
				
			||||||
      <%= gettext("No ammo groups in this container") %>
 | 
					      <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 %>
 | 
				
			||||||
 | 
					      <h2 class="mx-4 title text-lg text-primary-600 text-center">
 | 
				
			||||||
 | 
					        <%= gettext("No ammo in this container") %>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
    <% else %>
 | 
					    <% else %>
 | 
				
			||||||
      <%= for ammo_group <- @container.ammo_groups do %>
 | 
					      <%= if @view_table do %>
 | 
				
			||||||
 | 
					        <.live_component
 | 
				
			||||||
 | 
					          module={CanneryWeb.Components.AmmoGroupTableComponent}
 | 
				
			||||||
 | 
					          id="ammo-type-show-table"
 | 
				
			||||||
 | 
					          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} />
 | 
					            <.ammo_group_card ammo_group={ammo_group} />
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
  </p>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action in [:edit] do %>
 | 
					<%= if @live_action in [:edit] do %>
 | 
				
			||||||
@@ -108,10 +157,10 @@
 | 
				
			|||||||
  </.modal>
 | 
					  </.modal>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action == :add_tag do %>
 | 
					<%= if @live_action == :edit_tags do %>
 | 
				
			||||||
  <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
 | 
					  <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
 | 
				
			||||||
    <.live_component
 | 
					    <.live_component
 | 
				
			||||||
      module={CanneryWeb.ContainerLive.AddTagComponent}
 | 
					      module={CanneryWeb.ContainerLive.EditTagsComponent}
 | 
				
			||||||
      id={@container.id}
 | 
					      id={@container.id}
 | 
				
			||||||
      title={@page_title}
 | 
					      title={@page_title}
 | 
				
			||||||
      action={@live_action}
 | 
					      action={@live_action}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,38 +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)
 | 
				
			||||||
    {:ok, socket |> assign_defaults(session) |> assign(query: "", results: %{}, admins: admins)}
 | 
					    socket = socket |> assign(page_title: gettext("Home"), admins: admins, version: @version)
 | 
				
			||||||
  end
 | 
					    {:ok, socket}
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @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}")}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      _ ->
 | 
					 | 
				
			||||||
        {:noreply,
 | 
					 | 
				
			||||||
         socket
 | 
					 | 
				
			||||||
         |> put_flash(:error, "No dependencies found matching \"#{query}\"")
 | 
					 | 
				
			||||||
         |> assign(results: %{}, query: query)}
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def render(assigns) do
 | 
					  def render(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl">
 | 
				
			||||||
      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>
 | 
				
			||||||
@@ -94,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 %>
 | 
				
			||||||
@@ -121,26 +113,64 @@ defmodule CanneryWeb.HomeLive do
 | 
				
			|||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <li class="flex flex-row justify-center space-x-2">
 | 
					        <li class="flex flex-row justify-center items-center space-x-2">
 | 
				
			||||||
          <b>Version:</b>
 | 
					          <b>Version:</b>
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            href="https://gitea.bubbletea.dev/shibao/cannery/src/branch/stable/CHANGELOG.md"
 | 
				
			||||||
 | 
					            class="flex flex-row justify-center items-center space-x-2 hover:underline"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <p>
 | 
					            <p>
 | 
				
			||||||
            0.1.0
 | 
					              <%= @version %>
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
 | 
					            <i class="fas fa-md fa-info-circle"></i>
 | 
				
			||||||
 | 
					          </.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,10 +3,10 @@
 | 
				
			|||||||
    <%= @title %>
 | 
					    <%= @title %>
 | 
				
			||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="invite-form"
 | 
					    id="invite-form"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
@@ -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}
 | 
				
			||||||
@@ -40,7 +38,7 @@ defmodule CanneryWeb.InviteLive.Index do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp apply_action(socket, :index, _params) do
 | 
					  defp apply_action(socket, :index, _params) do
 | 
				
			||||||
    socket |> assign(page_title: gettext("Listing Invites"), invite: nil)
 | 
					    socket |> assign(page_title: gettext("Invites"), invite: nil)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
@@ -119,7 +117,7 @@ defmodule CanneryWeb.InviteLive.Index do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def handle_event("copy_to_clipboard", _, socket) do
 | 
					  def handle_event("copy_to_clipboard", _params, socket) do
 | 
				
			||||||
    prompt = dgettext("prompts", "Copied to clipboard")
 | 
					    prompt = dgettext("prompts", "Copied to clipboard")
 | 
				
			||||||
    {:noreply, socket |> put_flash(:info, prompt)}
 | 
					    {:noreply, socket |> put_flash(:info, prompt)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,32 +27,44 @@ defmodule CanneryWeb.LiveHelpers do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  def modal(assigns) do
 | 
					  def modal(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div
 | 
					    <.link
 | 
				
			||||||
      id="modal"
 | 
					      patch={@return_to}
 | 
				
			||||||
 | 
					      id="modal-bg"
 | 
				
			||||||
      class="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"
 | 
					         p-8 flex flex-col justify-center items-center cursor-auto"
 | 
				
			||||||
      style="opacity: 1 !important; 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()}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span class="hidden"></span>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      id="modal"
 | 
				
			||||||
 | 
					      class="fixed z-10 left-0 top-0 pointer-events-none
 | 
				
			||||||
 | 
					        w-full h-full overflow-hidden
 | 
				
			||||||
 | 
					        p-4 sm:p-8 flex flex-col justify-center items-center"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        id="modal-content"
 | 
					        id="modal-content"
 | 
				
			||||||
        class="fade-in-scale w-full max-w-3xl max-h-128 relative overflow-y-auto
 | 
					        class="fade-in-scale w-full max-w-3xl relative
 | 
				
			||||||
 | 
					        pointer-events-auto overflow-hidden
 | 
				
			||||||
 | 
					        px-8 py-4 sm:py-8 flex flex-col justify-center items-center
 | 
				
			||||||
        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"
 | 
				
			||||||
        phx-click-away={hide_modal()}
 | 
					 | 
				
			||||||
        phx-window-keydown={hide_modal()}
 | 
					 | 
				
			||||||
        phx-key="escape"
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <%= live_patch to: @return_to,
 | 
					        <.link
 | 
				
			||||||
                   id: "close",
 | 
					          patch={@return_to}
 | 
				
			||||||
                   class:
 | 
					          id="close"
 | 
				
			||||||
                     "absolute top-8 right-10 text-gray-500 hover:text-gray-800 transition-all duration-500 ease-in-out",
 | 
					          class="absolute top-8 right-10
 | 
				
			||||||
                   phx_click: hide_modal() do %>
 | 
					                      text-gray-500 hover:text-gray-800
 | 
				
			||||||
 | 
					                      transition-all duration-500 ease-in-out"
 | 
				
			||||||
 | 
					          phx_remove={hide_modal()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <i class="fa-fw fa-lg fas fa-times"></i>
 | 
					          <i class="fa-fw fa-lg fas fa-times"></i>
 | 
				
			||||||
        <% end %>
 | 
					        </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="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) %>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
@@ -71,9 +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-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,10 +4,10 @@
 | 
				
			|||||||
  </h2>
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    id="shot-group-form"
 | 
					    id="shot-group-form"
 | 
				
			||||||
    class="flex flex-col 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"
 | 
				
			||||||
    phx-target={@myself}
 | 
					    phx-target={@myself}
 | 
				
			||||||
    phx-change="validate"
 | 
					    phx-change="validate"
 | 
				
			||||||
    phx-submit="save"
 | 
					    phx-submit="save"
 | 
				
			||||||
@@ -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,16 +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)
 | 
				
			||||||
    socket |> assign(shot_groups: shot_groups, ammo_groups: ammo_groups)
 | 
					    chart_data = shot_groups |> get_chart_data_for_shot_group()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket
 | 
				
			||||||
 | 
					    |> assign(
 | 
				
			||||||
 | 
					      ammo_groups: ammo_groups,
 | 
				
			||||||
 | 
					      chart_data: chart_data,
 | 
				
			||||||
 | 
					      shot_groups: shot_groups
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_chart_data_for_shot_group([ShotGroup.t()]) :: [map()]
 | 
				
			||||||
 | 
					  defp get_chart_data_for_shot_group(shot_groups) do
 | 
				
			||||||
 | 
					    shot_groups
 | 
				
			||||||
 | 
					    |> Repo.preload(ammo_group: :ammo_type)
 | 
				
			||||||
 | 
					    |> Enum.group_by(fn %{date: date} -> date end, fn %{count: count} -> count end)
 | 
				
			||||||
 | 
					    |> Enum.map(fn {date, rounds} ->
 | 
				
			||||||
 | 
					      sum = Enum.sum(rounds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      %{
 | 
				
			||||||
 | 
					        date: date,
 | 
				
			||||||
 | 
					        count: sum,
 | 
				
			||||||
 | 
					        label: gettext("Rounds shot: %{count}", count: sum)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Enum.sort_by(fn %{date: date} -> date end, Date)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,16 +9,15 @@
 | 
				
			|||||||
      <%= 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>
 | 
				
			||||||
    ) %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="w-full flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
      <%= for ammo_group <- @ammo_groups do %>
 | 
					      <%= for ammo_group <- @ammo_groups do %>
 | 
				
			||||||
        <.ammo_group_card ammo_group={ammo_group}>
 | 
					        <.ammo_group_card ammo_group={ammo_group}>
 | 
				
			||||||
          <button
 | 
					          <button
 | 
				
			||||||
@@ -29,21 +28,24 @@
 | 
				
			|||||||
            data-confirm={"#{dgettext("prompts", "Are you sure you want to unstage this ammo?")}"}
 | 
					            data-confirm={"#{dgettext("prompts", "Are you sure you want to unstage this ammo?")}"}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <%= 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", "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"
 | 
				
			||||||
        ) %>
 | 
					          >
 | 
				
			||||||
 | 
					            <%= dgettext("actions", "Record shots") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
        </.ammo_group_card>
 | 
					        </.ammo_group_card>
 | 
				
			||||||
      <% end %>
 | 
					      <% 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,70 +55,76 @@
 | 
				
			|||||||
      <%= gettext("Shot log") %>
 | 
					      <%= gettext("Shot log") %>
 | 
				
			||||||
    </h1>
 | 
					    </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
 | 
					    <canvas
 | 
				
			||||||
      <table class="min-w-full table-auto text-center bg-white">
 | 
					      id="shot-log-chart"
 | 
				
			||||||
        <thead class="border-b border-primary-600">
 | 
					      phx-hook="ShotLogChart"
 | 
				
			||||||
          <tr>
 | 
					      phx-update="ignore"
 | 
				
			||||||
            <th class="p-2">
 | 
					      class="max-h-72"
 | 
				
			||||||
              <%= gettext("Ammo") %>
 | 
					      data-chart-data={Jason.encode!(@chart_data)}
 | 
				
			||||||
            </th>
 | 
					      data-label={gettext("Rounds shot")}
 | 
				
			||||||
            <th class="p-2">
 | 
					      data-color={random_color()}
 | 
				
			||||||
              <%= gettext("Rounds shot") %>
 | 
					      aria-label={gettext("Rounds shot chart")}
 | 
				
			||||||
            </th>
 | 
					      role="img"
 | 
				
			||||||
            <th class="p-2">
 | 
					    >
 | 
				
			||||||
              <%= gettext("Notes") %>
 | 
					      <%= dgettext("errors", "Your browser does not support the canvas element.") %>
 | 
				
			||||||
            </th>
 | 
					    </canvas>
 | 
				
			||||||
            <th class="p-2">
 | 
					 | 
				
			||||||
              <%= gettext("Date") %>
 | 
					 | 
				
			||||||
            </th>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <th class="p-2"></th>
 | 
					    <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">
 | 
				
			||||||
          </tr>
 | 
					      <.form
 | 
				
			||||||
        </thead>
 | 
					        :let={f}
 | 
				
			||||||
        <tbody id="shot_groups">
 | 
					        for={:search}
 | 
				
			||||||
          <%= for shot_group <- @shot_groups do %>
 | 
					        phx-change="search"
 | 
				
			||||||
            <tr id={"shot_group-#{shot_group.id}"}>
 | 
					        phx-submit="search"
 | 
				
			||||||
              <td class="p-2">
 | 
					        class="grow self-stretch flex flex-col items-stretch"
 | 
				
			||||||
                <%= live_patch(shot_group.ammo_group.ammo_type.name,
 | 
					        data-qa="shot_group_search"
 | 
				
			||||||
                  to: Routes.ammo_group_show_path(Endpoint, :show, shot_group.ammo_group),
 | 
					      >
 | 
				
			||||||
                  class: "link"
 | 
					        <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					          class: "input input-primary",
 | 
				
			||||||
 | 
					          value: @search,
 | 
				
			||||||
 | 
					          phx_debounce: 300,
 | 
				
			||||||
 | 
					          placeholder: gettext("Search shot records")
 | 
				
			||||||
        ) %>
 | 
					        ) %>
 | 
				
			||||||
              </td>
 | 
					      </.form>
 | 
				
			||||||
              <td class="p-2">
 | 
					    </div>
 | 
				
			||||||
                <%= shot_group.count %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= shot_group.notes %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td class="p-2">
 | 
					 | 
				
			||||||
                <%= shot_group.date |> display_date() %>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <td class="p-2 w-full h-full space-x-2 flex justify-center items-center">
 | 
					    <%= 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">
 | 
					          <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),
 | 
					            <.link
 | 
				
			||||||
                             class: "text-primary-600 link",
 | 
					              patch={Routes.range_index_path(Endpoint, :edit, 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",
 | 
					              class="text-primary-600 link"
 | 
				
			||||||
                       phx_value_id: shot_group.id,
 | 
					              phx-click="delete"
 | 
				
			||||||
                       data: [
 | 
					              phx-value-id={shot_group.id}
 | 
				
			||||||
                         confirm: dgettext("prompts", "Are you sure you want to delete this shot record?"),
 | 
					              data-confirm={
 | 
				
			||||||
                         qa: "delete-#{shot_group.id}"
 | 
					                dgettext("prompts", "Are you sure you want to delete this shot record?")
 | 
				
			||||||
                       ] do %>
 | 
					              }
 | 
				
			||||||
 | 
					              data-qa={"delete-#{shot_group.id}"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
					              <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
                  <% end %>
 | 
					            </.link>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
              </td>
 | 
					        </:actions>
 | 
				
			||||||
            </tr>
 | 
					      </.live_component>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
        </tbody>
 | 
					 | 
				
			||||||
      </table>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  <% 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,10 +58,10 @@ 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 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"
 | 
				
			||||||
        phx-target={@myself}
 | 
					        phx-target={@myself}
 | 
				
			||||||
        phx-change="validate"
 | 
					        phx-change="validate"
 | 
				
			||||||
        phx-submit="save"
 | 
					        phx-submit="save"
 | 
				
			||||||
@@ -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,11 +6,15 @@ 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
 | 
				
			||||||
@@ -20,18 +24,38 @@ defmodule CanneryWeb.TagLive.Index do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  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("Listing 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 %>
 | 
					 | 
				
			||||||
  <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 %>
 | 
					  <% 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"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= text_input(f, :search_term,
 | 
				
			||||||
 | 
					        class: "input input-primary",
 | 
				
			||||||
 | 
					        value: @search,
 | 
				
			||||||
 | 
					        phx_debounce: 300,
 | 
				
			||||||
 | 
					        placeholder: gettext("Search tags")
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					    </.form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= if @tags |> Enum.empty?() do %>
 | 
				
			||||||
 | 
					    <h2 class="title text-xl text-primary-600">
 | 
				
			||||||
 | 
					      <%= gettext("No tags") %>
 | 
				
			||||||
 | 
					      <%= display_emoji("😔") %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					  <% else %>
 | 
				
			||||||
 | 
					    <div class="flex flex-row flex-wrap justify-center items-stretch">
 | 
				
			||||||
 | 
					      <%= 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>
 | 
					            <i class="fa-fw fa-lg fas fa-trash"></i>
 | 
				
			||||||
        <% end %>
 | 
					          </.link>
 | 
				
			||||||
        </.tag_card>
 | 
					        </.tag_card>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= if @live_action in [:new, :edit] do %>
 | 
					<%= if @live_action in [:new, :edit] do %>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,19 @@ 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
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
 | 
				
			||||||
 | 
					    default = Application.fetch_env!(:gettext, :default_locale)
 | 
				
			||||||
 | 
					    Gettext.put_locale(locale || default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, locale || default)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp put_user_locale(conn, _opts) do
 | 
				
			||||||
 | 
					    default = Application.fetch_env!(:gettext, :default_locale)
 | 
				
			||||||
 | 
					    Gettext.put_locale(default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, default)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pipeline :require_admin do
 | 
					  pipeline :require_admin do
 | 
				
			||||||
@@ -49,40 +62,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/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/add_tag", ContainerLive.Show, :add_tag
 | 
					    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/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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,8 @@
 | 
				
			|||||||
      <%= dgettext("errors", "Error") %>| Cannery
 | 
					      <%= dgettext("errors", "Error") %>| Cannery
 | 
				
			||||||
    </title>
 | 
					    </title>
 | 
				
			||||||
    <link rel="stylesheet" href="/css/app.css" />
 | 
					    <link rel="stylesheet" href="/css/app.css" />
 | 
				
			||||||
    <script defer type="text/javascript" src="/js/app.js"></script>
 | 
					    <script defer type="text/javascript" src="/js/app.js">
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body class="pb-8 m-0 p-0 w-full h-full">
 | 
					  <body class="pb-8 m-0 p-0 w-full h-full">
 | 
				
			||||||
    <header>
 | 
					    <header>
 | 
				
			||||||
@@ -16,9 +17,7 @@
 | 
				
			|||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="pb-8 w-full flex flex-col justify-center items-center text-center">
 | 
					    <div class="pb-8 w-full flex flex-col justify-center items-center text-center">
 | 
				
			||||||
      <div
 | 
					      <div class="p-8 sm:p-16 w-full flex flex-col justify-center items-center space-y-4 max-w-3xl">
 | 
				
			||||||
        class="p-8 sm:p-16 w-full flex flex-col justify-center items-center space-y-4 max-w-3xl"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <h1 class="title text-primary-600 text-3xl">
 | 
					        <h1 class="title text-primary-600 text-3xl">
 | 
				
			||||||
          <%= @error_string %>
 | 
					          <%= @error_string %>
 | 
				
			||||||
        </h1>
 | 
					        </h1>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,14 +4,10 @@
 | 
				
			|||||||
      <%= @email.subject %>
 | 
					      <%= @email.subject %>
 | 
				
			||||||
    </title>
 | 
					    </title>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body
 | 
					  <body style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;">
 | 
				
			||||||
    style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <%= @inner_content %>
 | 
					    <%= @inner_content %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <hr
 | 
					    <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;" />
 | 
				
			||||||
      style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, HomeLive)}>
 | 
					    <a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, HomeLive)}>
 | 
				
			||||||
      <%= dgettext(
 | 
					      <%= dgettext(
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user