Compare commits
	
		
			98 Commits
		
	
	
		
			0.9.0
			...
			202b70dc66
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 202b70dc66 | |||
| b963baa49d | |||
| 70701a27d3 | |||
| 67dc16d222 | |||
| fa35038426 | |||
| d896257602 | |||
| 4ca51a3f53 | |||
| 96b05e8332 | |||
| 557a2cac3d | |||
| e16e04c114 | |||
| bbe4d82303 | |||
| c69d7843ab | |||
| c18f59050c | |||
| 67d688fc1e | |||
|  | 28e5fa56c3 | ||
|  | e301d3dd17 | ||
|  | 4881cf6edb | ||
|  | 6b61c45889 | ||
|  | 4a674a0504 | ||
| 7e6959fb3b | |||
| 22f13b0c57 | |||
| 31024dcc0d | |||
| e843014502 | |||
| 5d146ce6af | |||
| 27cda3733e | |||
| 1965ecba32 | |||
| 69e40c6d18 | |||
| 34b4b24e67 | |||
| 7ebed8d4c0 | |||
| b5619b8606 | |||
| ef28de53a1 | |||
| fcd5dbc605 | |||
| 7738e68292 | |||
| df645a6188 | |||
| bed4fbaf54 | |||
| f94ef0a2ca | |||
| 7cdb8af690 | |||
| 52c4ab45d5 | |||
| a35f43d6df | |||
| 9edeb1e803 | |||
| 7e55446b3e | |||
| 9643e9f46d | |||
| 8466fcd1f9 | |||
| e713a2e108 | |||
| a8fa321040 | |||
| f0536f3030 | |||
| a94d2eebf4 | |||
| cfc56519f5 | |||
| e80c2018be | |||
| 71fdd42d96 | |||
| 8e99a57994 | |||
| 7c42dd8a3a | |||
| 79c97d7502 | |||
| 2e488fa26c | |||
| 2179bd5d86 | |||
| 49628cb9bb | |||
| 8a58d53dc1 | |||
| 9306d0f970 | |||
| 763c86a379 | |||
| b85b1735c0 | |||
| ab1a288928 | |||
| e6ef0a8c68 | |||
| beeaf521c5 | |||
| 8cb6068b85 | |||
| 334d841d57 | |||
| 1037f37be2 | |||
| 1796fb822f | |||
| 8ed64f9c87 | |||
| dd4a9f7119 | |||
| dbafaad500 | |||
| eb4ce07b5f | |||
| 2b7550a954 | |||
| abaccac9f0 | |||
| 2b81978adb | |||
| 17bfe1a987 | |||
| 81f21a02c4 | |||
| 9a17d4ad24 | |||
| 9835fe3f5e | |||
| 4dee8808f3 | |||
| 65c70ca398 | |||
| 550f6a6420 | |||
| 88c3f15fc8 | |||
| c33f15603b | |||
| 98775359af | |||
| e0e7b25bc4 | |||
| bdddf65685 | |||
| 6f50702b11 | |||
| 5f8d1a917f | |||
| 32801828fa | |||
| 6ed3312ea8 | |||
| b122253b9b | |||
| a68a16bf06 | |||
| 4b6d0952f8 | |||
| 0544b58ab6 | |||
| 6d26103784 | |||
| 0cae7c2940 | |||
| 1e645b5bb8 | |||
| bab2b26c13 | 
| @@ -17,7 +17,7 @@ steps: | |||||||
|       - .mix |       - .mix | ||||||
|  |  | ||||||
| - name: test | - name: test | ||||||
|   image: elixir:1.14.1-alpine |   image: elixir:1.17.0-otp-27-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 | ||||||
| @@ -42,7 +42,8 @@ steps: | |||||||
|     repo: shibaobun/cannery |     repo: shibaobun/cannery | ||||||
|     purge: true |     purge: true | ||||||
|     compress: true |     compress: true | ||||||
|     platforms: linux/amd64,linux/arm/v7 |     platforms: | ||||||
|  |       - linux/amd64 | ||||||
|     username: |     username: | ||||||
|       from_secret: docker_username |       from_secret: docker_username | ||||||
|     password: |     password: | ||||||
| @@ -59,7 +60,8 @@ steps: | |||||||
|     repo: shibaobun/cannery |     repo: shibaobun/cannery | ||||||
|     purge: true |     purge: true | ||||||
|     compress: true |     compress: true | ||||||
|     platforms: linux/amd64,linux/arm/v7 |     platforms: | ||||||
|  |       - linux/amd64 | ||||||
|     username: |     username: | ||||||
|       from_secret: docker_username |       from_secret: docker_username | ||||||
|     password: |     password: | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [ | [ | ||||||
|   import_deps: [:ecto, :phoenix], |   import_deps: [:ecto, :ecto_sql, :phoenix], | ||||||
|   inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"], |   inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"], | ||||||
|   subdirectories: ["priv/*/migrations"], |   subdirectories: ["priv/*/migrations"], | ||||||
|   plugins: [Phoenix.LiveView.HTMLFormatter] |   plugins: [Phoenix.LiveView.HTMLFormatter] | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -1,3 +1,3 @@ | |||||||
| elixir 1.14.1-otp-25 | elixir 1.17.0-otp-27 | ||||||
| erlang 25.1.2 | erlang 27.0 | ||||||
| nodejs 18.9.1 | nodejs 22.3.0 | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,55 @@ | |||||||
|  | # v0.9.10 | ||||||
|  | - Fix issue with logger failing on oban exceptions | ||||||
|  | - Fix an issue with emails not being able to be sent | ||||||
|  | - Update deps | ||||||
|  |  | ||||||
|  | # v0.9.9 | ||||||
|  | - Actually fix bar graph | ||||||
|  |  | ||||||
|  | # v0.9.8 | ||||||
|  | - Make bar graph ignore empty days | ||||||
|  | - Update dependencies | ||||||
|  |  | ||||||
|  | # v0.9.7 | ||||||
|  | - Fix margin on bottom of page | ||||||
|  | - Use bar graph instead of line graph | ||||||
|  | - Improve login page autocomplete behavior | ||||||
|  |  | ||||||
|  | # v0.9.6 | ||||||
|  | - Make ammo packs in containers directly navigable in table view | ||||||
|  | - Update dependencies | ||||||
|  |  | ||||||
|  | # v0.9.5 | ||||||
|  | - Update dependencies | ||||||
|  |  | ||||||
|  | # v0.9.4 | ||||||
|  | - Code quality fixes | ||||||
|  | - Fix error/404 pages not rendering properly | ||||||
|  | - Update dependencies | ||||||
|  | - Fix Range page title | ||||||
|  |  | ||||||
|  | # v0.9.3 | ||||||
|  | - Update dependencies | ||||||
|  | - Add pack lot number to search | ||||||
|  | - Improve tests | ||||||
|  | - Change invite path slightly | ||||||
|  | - Disable arm builds since ci fails to build | ||||||
|  |  | ||||||
|  | # v0.9.2 | ||||||
|  | - Add lot number to packs | ||||||
|  | - Don't show price paid and lot number columns when displaying packs if not used | ||||||
|  | - Fix additional shotgun fields not being exportable | ||||||
|  | - Fixes duplicate chamber size column for ammo types | ||||||
|  | - Hide bullet type field when editing/creating shotgun ammo types | ||||||
|  | - Fix ammo type creation not displaying all the necessary fields on first load | ||||||
|  |  | ||||||
|  | # v0.9.1 | ||||||
|  | - Rename ammo type's "type" to "class" to avoid confusion | ||||||
|  | - Rename "ammo type" to "type" to avoid confusion | ||||||
|  | - Fixes type search | ||||||
|  | - Fixes shot records table disappearing after selecting an empty ammo class | ||||||
|  | - Code quality improvements | ||||||
|  |  | ||||||
| # v0.9.0 | # v0.9.0 | ||||||
| - Add length limits to all string fields | - Add length limits to all string fields | ||||||
| - Add selectable ammo types | - Add selectable ammo types | ||||||
| @@ -51,7 +103,7 @@ | |||||||
| # v0.8.0 | # v0.8.0 | ||||||
| - Add search to catalog, ammo, container, tag and range index pages | - Add search to catalog, ammo, container, tag and range index pages | ||||||
| - Tweak urls for catalog, ammo, containers, tags and shot records | - Tweak urls for catalog, ammo, containers, tags and shot records | ||||||
| - Fix bug with shot group chart not drawing lines between days correctly | - Fix bug with shot record chart not drawing lines between days correctly | ||||||
| - Improve cards across app (make them line up with each other) | - Improve cards across app (make them line up with each other) | ||||||
| - Update translations and add spanish!!! (thank you Brea and Hannah!) | - Update translations and add spanish!!! (thank you Brea and Hannah!) | ||||||
|  |  | ||||||
| @@ -63,7 +115,7 @@ | |||||||
| - Fix toggle button styling | - Fix toggle button styling | ||||||
| - Miscellanous code improvements | - Miscellanous code improvements | ||||||
| - Improve container index table | - Improve container index table | ||||||
| - Fix bug with ammo not updating after deleting shot group | - Fix bug with ammo not updating after deleting shot record | ||||||
| - Replace ammo "added on" with "purchased on" | - Replace ammo "added on" with "purchased on" | ||||||
| - Miscellaneous wording improvements | - Miscellaneous wording improvements | ||||||
| - Update translations | - Update translations | ||||||
| @@ -72,8 +124,8 @@ | |||||||
| - Add shading to table component | - Add shading to table component | ||||||
| - Fix chart to sum by day | - Fix chart to sum by day | ||||||
| - Fix whitespace when copying invite url | - Fix whitespace when copying invite url | ||||||
| - Make ammo type show page also display ammo groups as table | - Make ammo type show page also display packs as table | ||||||
| - Make container show page also display ammo groups as table | - Make container show page also display packs as table | ||||||
| - Display CPR for ammo packs | - Display CPR for ammo packs | ||||||
| - Add original count for ammo packs | - Add original count for ammo packs | ||||||
| - Add ammo pack CPR and original count to json export | - Add ammo pack CPR and original count to json export | ||||||
| @@ -97,7 +149,7 @@ | |||||||
| - Add ammo type cloning | - Add ammo type cloning | ||||||
| - Add container cloning | - Add container cloning | ||||||
| - Fix bug with moving ammo packs between containers | - Fix bug with moving ammo packs between containers | ||||||
| - Add button to set rounds left to 0 when creating a shot group | - Add button to set rounds left to 0 when creating a shot record | ||||||
| - Update project dependencies | - Update project dependencies | ||||||
|  |  | ||||||
| # v0.5.4 | # v0.5.4 | ||||||
| @@ -149,8 +201,8 @@ | |||||||
| # v0.3.0 | # v0.3.0 | ||||||
| - Fix ammo type counts not showing when count is 0 | - Fix ammo type counts not showing when count is 0 | ||||||
| - Add prompt to create first container before first ammo group | - Add prompt to create first container before first ammo group | ||||||
| - Edit and delete shot groups from ammo group show page | - Edit and delete shot records from ammo group show page | ||||||
| - Use today's date when adding new shot groups | - Use today's date when adding new shot records | ||||||
| - Create multiple ammo groups at one time | - Create multiple ammo groups at one time | ||||||
|  |  | ||||||
| # v0.2.3 | # v0.2.3 | ||||||
|   | |||||||
| @@ -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 `AmmoGroup` in the app, please alias |   - I.e. since there's only one `Pack` in the app, please alias | ||||||
|     `AmmoGroup.t()` instead of using `Cannery.Ammo.AmmoGroup.t()` |     `Pack.t()` instead of using `Cannery.Ammo.Pack.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 | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM elixir:1.14.1-alpine AS build | FROM elixir:1.17.0-otp-27-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 libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs |     apk add --no-cache bash openssl libssl3 libcrypto3 libgcc libstdc++ ncurses-libs | ||||||
|  |  | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ The self-hosted firearm tracker website. | |||||||
| # Features | # Features | ||||||
|  |  | ||||||
| - Create containers to store your ammunition, and tag them with custom tags | - Create containers to store your ammunition, and tag them with custom tags | ||||||
| - Add ammunition types to Cannery, and then ammunition groups to your containers | - Add ammunition types to Cannery, and then ammo packs to your containers | ||||||
| - Stage groups of ammo for range day and record your ammo usage | - Stage ammo packs for range day and track your usage with shot records | ||||||
| - Invitations via invite tokens or public registration | - Invitations via invite tokens or public registration | ||||||
|  |  | ||||||
| # Installation | # Installation | ||||||
|   | |||||||
| @@ -25,7 +25,6 @@ import 'phoenix_html' | |||||||
| import { Socket } from 'phoenix' | import { Socket } from 'phoenix' | ||||||
| import { LiveSocket } from 'phoenix_live_view' | import { LiveSocket } from 'phoenix_live_view' | ||||||
| import topbar from 'topbar' | import topbar from 'topbar' | ||||||
| import MaintainAttrs from './maintain_attrs' |  | ||||||
| import ShotLogChart from './shot_log_chart' | import ShotLogChart from './shot_log_chart' | ||||||
| import Date from './date' | import Date from './date' | ||||||
| import DateTime from './datetime' | import DateTime from './datetime' | ||||||
| @@ -33,7 +32,7 @@ import DateTime from './datetime' | |||||||
| const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') | const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') | ||||||
| const liveSocket = new LiveSocket('/live', Socket, { | const liveSocket = new LiveSocket('/live', Socket, { | ||||||
|   params: { _csrf_token: csrfToken }, |   params: { _csrf_token: csrfToken }, | ||||||
|   hooks: { Date, DateTime, MaintainAttrs, ShotLogChart } |   hooks: { Date, DateTime, ShotLogChart } | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // Show progress bar on live navigation and form submits | // Show progress bar on live navigation and form submits | ||||||
|   | |||||||
| @@ -1,11 +0,0 @@ | |||||||
| // maintain user adjusted attributes, like textbox length on phoenix liveview |  | ||||||
| // update. https://github.com/phoenixframework/phoenix_live_view/issues/1011 |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   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)]) }, |  | ||||||
|   updated () { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) } |  | ||||||
| } |  | ||||||
| @@ -1,13 +1,12 @@ | |||||||
| import { Chart, Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale } from 'chart.js' | import Chart from 'chart.js/auto' | ||||||
| import 'chartjs-adapter-date-fns' | import 'chartjs-adapter-date-fns' | ||||||
| Chart.register(Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale) |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   initalizeChart (el) { |   initalizeChart (el) { | ||||||
|     const data = JSON.parse(el.dataset.chartData) |     const data = JSON.parse(el.dataset.chartData) | ||||||
|  |  | ||||||
|     this.el.chart = new Chart(el, { |     this.el.chart = new Chart(el, { | ||||||
|       type: 'line', |       type: 'bar', | ||||||
|       data: { |       data: { | ||||||
|         datasets: [{ |         datasets: [{ | ||||||
|           label: el.dataset.label, |           label: el.dataset.label, | ||||||
| @@ -51,13 +50,17 @@ export default { | |||||||
|             stacked: true, |             stacked: true, | ||||||
|             grace: '15%', |             grace: '15%', | ||||||
|             ticks: { |             ticks: { | ||||||
|               padding: 15 |               padding: 15, | ||||||
|  |               precision: 0 | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           x: { |           x: { | ||||||
|             type: 'time', |             type: 'timeseries', | ||||||
|             time: { |             time: { | ||||||
|               unit: 'day' |               unit: 'day' | ||||||
|  |             }, | ||||||
|  |             ticks: { | ||||||
|  |               source: 'data' | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|   | |||||||
							
								
								
									
										19332
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19332
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,8 +3,8 @@ | |||||||
|   "description": " ", |   "description": " ", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "engines": { |   "engines": { | ||||||
|     "node": "v18.9.1", |     "node": "v22.3.0", | ||||||
|     "npm": "8.19.1" |     "npm": "10.8.1" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "deploy": "NODE_ENV=production webpack --mode production", |     "deploy": "NODE_ENV=production webpack --mode production", | ||||||
| @@ -13,37 +13,37 @@ | |||||||
|     "test": "standard" |     "test": "standard" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@fortawesome/fontawesome-free": "^6.3.0", |     "@fortawesome/fontawesome-free": "^6.5.2", | ||||||
|     "chart.js": "^4.2.1", |     "chart.js": "^4.4.3", | ||||||
|     "chartjs-adapter-date-fns": "^3.0.0", |     "chartjs-adapter-date-fns": "^3.0.0", | ||||||
|     "date-fns": "^2.29.3", |     "date-fns": "^3.6.0", | ||||||
|     "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": "^2.0.1" |     "topbar": "^3.0.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "^7.21.3", |     "@babel/core": "^7.24.7", | ||||||
|     "@babel/preset-env": "^7.20.2", |     "@babel/preset-env": "^7.24.7", | ||||||
|     "autoprefixer": "^10.4.14", |     "autoprefixer": "^10.4.19", | ||||||
|     "babel-loader": "^9.1.2", |     "babel-loader": "^9.1.3", | ||||||
|     "copy-webpack-plugin": "^11.0.0", |     "copy-webpack-plugin": "^12.0.2", | ||||||
|     "css-loader": "^6.7.3", |     "css-loader": "^7.1.2", | ||||||
|     "css-minimizer-webpack-plugin": "^4.2.2", |     "css-minimizer-webpack-plugin": "^7.0.0", | ||||||
|     "file-loader": "^6.2.0", |     "file-loader": "^6.2.0", | ||||||
|     "mini-css-extract-plugin": "^2.7.5", |     "mini-css-extract-plugin": "^2.9.0", | ||||||
|     "npm-check-updates": "^16.7.12", |     "npm-check-updates": "^16.14.20", | ||||||
|     "postcss": "^8.4.21", |     "postcss": "^8.4.38", | ||||||
|     "postcss-import": "^15.1.0", |     "postcss-import": "^16.1.0", | ||||||
|     "postcss-loader": "^7.1.0", |     "postcss-loader": "^8.1.1", | ||||||
|     "postcss-preset-env": "^8.0.1", |     "postcss-preset-env": "^9.5.14", | ||||||
|     "sass": "^1.59.3", |     "sass": "^1.77.5", | ||||||
|     "sass-loader": "^13.2.1", |     "sass-loader": "^14.2.1", | ||||||
|     "standard": "^17.0.0", |     "standard": "^17.1.0", | ||||||
|     "tailwindcss": "^3.2.7", |     "tailwindcss": "^3.4.4", | ||||||
|     "terser-webpack-plugin": "^5.3.7", |     "terser-webpack-plugin": "^5.3.10", | ||||||
|     "webpack": "^5.76.2", |     "webpack": "^5.92.0", | ||||||
|     "webpack-cli": "^5.0.1", |     "webpack-cli": "^5.1.4", | ||||||
|     "webpack-dev-server": "^4.13.1" |     "webpack-dev-server": "^5.0.4" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ 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])?$/, | ||||||
|           type: 'asset/resource', |           type: 'asset/resource', | ||||||
|           generator: { filename: 'fonts/[name][ext]' } |           generator: { filename: 'fonts/[name].[ext]' } | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -18,7 +18,10 @@ config :cannery, CanneryWeb.Endpoint, | |||||||
|   url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"], |   url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"], | ||||||
|   http: [port: String.to_integer(System.get_env("PORT") || "4000")], |   http: [port: String.to_integer(System.get_env("PORT") || "4000")], | ||||||
|   secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I", |   secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I", | ||||||
|   render_errors: [view: CanneryWeb.ErrorView, accepts: ~w(html json), layout: false], |   render_errors: [ | ||||||
|  |     formats: [html: CanneryWeb.ErrorHTML, json: CanneryWeb.ErrorJSON], | ||||||
|  |     layout: false | ||||||
|  |   ], | ||||||
|   pubsub_server: Cannery.PubSub, |   pubsub_server: Cannery.PubSub, | ||||||
|   live_view: [signing_salt: "zOLgd3lr"] |   live_view: [signing_salt: "zOLgd3lr"] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -59,8 +59,7 @@ config :cannery, CanneryWeb.Endpoint, | |||||||
|     patterns: [ |     patterns: [ | ||||||
|       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", |       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", | ||||||
|       ~r"priv/gettext/.*(po)$", |       ~r"priv/gettext/.*(po)$", | ||||||
|       ~r"lib/cannery_web/(live|views)/.*(ex)$", |       ~r"lib/cannery_web/*/.*(ex)$" | ||||||
|       ~r"lib/cannery_web/templates/.*(eex)$" |  | ||||||
|     ] |     ] | ||||||
|   ] |   ] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do | |||||||
|   config :cannery, CanneryWeb.Endpoint, server: true |   config :cannery, CanneryWeb.Endpoint, server: true | ||||||
| end | end | ||||||
|  |  | ||||||
| config :cannery, CanneryWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true" | config :cannery, CanneryWeb.HTMLHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true" | ||||||
|  |  | ||||||
| # Set default locale | # Set default locale | ||||||
| config :gettext, :default_locale, System.get_env("LOCALE", "en_US") | config :gettext, :default_locale, System.get_env("LOCALE", "en_US") | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test | |||||||
| config :cannery, Cannery.Accounts, registration: "public" | config :cannery, Cannery.Accounts, registration: "public" | ||||||
|  |  | ||||||
| # Print only warnings and errors during test | # Print only warnings and errors during test | ||||||
| config :logger, level: :warn | config :logger, level: :warning | ||||||
|  |  | ||||||
| # Initialize plugs at runtime for faster test compilation | # Initialize plugs at runtime for faster test compilation | ||||||
| config :phoenix, :plug_init_mode, :runtime | config :phoenix, :plug_init_mode, :runtime | ||||||
|   | |||||||
| @@ -374,8 +374,8 @@ 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(token :: String.t()) :: :ok |   @spec delete_user_session_token(token :: String.t()) :: :ok | ||||||
|   def delete_session_token(token) do |   def delete_user_session_token(token) do | ||||||
|     UserToken.token_and_context_query(token, "session") |> Repo.delete_all() |     UserToken.token_and_context_query(token, "session") |> Repo.delete_all() | ||||||
|     :ok |     :ok | ||||||
|   end |   end | ||||||
| @@ -404,15 +404,15 @@ defmodule Cannery.Accounts do | |||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> is_admin?(%User{role: :admin}) |       iex> admin?(%User{role: :admin}) | ||||||
|       true |       true | ||||||
|  |  | ||||||
|       iex> is_admin?(%User{}) |       iex> admin?(%User{}) | ||||||
|       false |       false | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec is_admin?(User.t()) :: boolean() |   @spec admin?(User.t()) :: boolean() | ||||||
|   def is_admin?(%User{id: user_id}) do |   def admin?(%User{id: user_id}) do | ||||||
|     Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin) |     Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin) | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -421,16 +421,16 @@ defmodule Cannery.Accounts do | |||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> is_already_admin?(%User{role: :admin}) |       iex> already_admin?(%User{role: :admin}) | ||||||
|       true |       true | ||||||
|  |  | ||||||
|       iex> is_already_admin?(%User{}) |       iex> already_admin?(%User{}) | ||||||
|       false |       false | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec is_already_admin?(User.t() | nil) :: boolean() |   @spec already_admin?(User.t() | nil) :: boolean() | ||||||
|   def is_already_admin?(%User{role: :admin}), do: true |   def already_admin?(%User{role: :admin}), do: true | ||||||
|   def is_already_admin?(_invalid_user), do: false |   def already_admin?(_invalid_user), do: false | ||||||
|  |  | ||||||
|   ## Confirmation |   ## Confirmation | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,14 +3,15 @@ defmodule Cannery.Email do | |||||||
|   Emails that can be sent using Swoosh. |   Emails that can be sent using Swoosh. | ||||||
|  |  | ||||||
|   You can find the base email templates at |   You can find the base email templates at | ||||||
|   `lib/cannery_web/templates/layout/email.html.heex` for html emails and |   `lib/cannery_web/components/layouts/email_html.html.heex` for html emails and | ||||||
|   `lib/cannery_web/templates/layout/email.txt.heex` for text emails. |   `lib/cannery_web/components/layouts/email_text.txt.eex` for text emails. | ||||||
|   """ |   """ | ||||||
|  |  | ||||||
|   use Phoenix.Swoosh, view: CanneryWeb.EmailView, layout: {CanneryWeb.LayoutView, :email} |   import Swoosh.Email | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|  |   import Phoenix.Template | ||||||
|   alias Cannery.Accounts.User |   alias Cannery.Accounts.User | ||||||
|   alias CanneryWeb.EmailView |   alias CanneryWeb.{EmailHTML, Layouts} | ||||||
|  |  | ||||||
|   @typedoc """ |   @typedoc """ | ||||||
|   Represents an HTML and text body email that can be sent |   Represents an HTML and text body email that can be sent | ||||||
| @@ -25,24 +26,36 @@ defmodule Cannery.Email do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t() |   @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t() | ||||||
|   def generate_email("welcome", user, %{"url" => url}) do |   def generate_email("welcome", user, %{url: url}) do | ||||||
|     user |     user | ||||||
|     |> base_email(dgettext("emails", "Confirm your Cannery account")) |     |> base_email(dgettext("emails", "Confirm your Cannery account")) | ||||||
|     |> render_body("confirm_email.html", %{user: user, url: url}) |     |> html_email(:confirm_email_html, %{user: user, url: url}) | ||||||
|     |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url})) |     |> text_email(:confirm_email_text, %{user: user, url: url}) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def generate_email("reset_password", user, %{"url" => url}) do |   def generate_email("reset_password", user, %{url: url}) do | ||||||
|     user |     user | ||||||
|     |> base_email(dgettext("emails", "Reset your Cannery password")) |     |> base_email(dgettext("emails", "Reset your Cannery password")) | ||||||
|     |> render_body("reset_password.html", %{user: user, url: url}) |     |> html_email(:reset_password_html, %{user: user, url: url}) | ||||||
|     |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url})) |     |> text_email(:reset_password_text, %{user: user, url: url}) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def generate_email("update_email", user, %{"url" => url}) do |   def generate_email("update_email", user, %{url: url}) do | ||||||
|     user |     user | ||||||
|     |> base_email(dgettext("emails", "Update your Cannery email")) |     |> base_email(dgettext("emails", "Update your Cannery email")) | ||||||
|     |> render_body("update_email.html", %{user: user, url: url}) |     |> html_email(:update_email_html, %{user: user, url: url}) | ||||||
|     |> text_body(EmailView.render("update_email.txt", %{user: user, url: url})) |     |> text_email(:update_email_text, %{user: user, url: url}) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp html_email(email, atom, assigns) do | ||||||
|  |     heex = apply(EmailHTML, atom, [assigns]) | ||||||
|  |     html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: heex) | ||||||
|  |     email |> html_body(html) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp text_email(email, atom, assigns) do | ||||||
|  |     heex = apply(EmailHTML, atom, [assigns]) | ||||||
|  |     text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: heex) | ||||||
|  |     email |> text_body(text) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ defmodule Cannery.EmailWorker do | |||||||
|   alias Cannery.{Accounts, Email, Mailer} |   alias Cannery.{Accounts, Email, Mailer} | ||||||
|  |  | ||||||
|   @impl Oban.Worker |   @impl Oban.Worker | ||||||
|   def perform(%Oban.Job{args: %{"email" => email, "user_id" => user_id, "attrs" => attrs}}) do |   def perform(%Oban.Job{args: %{email: email, user_id: user_id, attrs: attrs}}) do | ||||||
|     Email.generate_email(email, user_id |> Accounts.get_user!(), attrs) |> Mailer.deliver() |     Email.generate_email(email, user_id |> Accounts.get_user!(), attrs) |> Mailer.deliver() | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -4,377 +4,436 @@ defmodule Cannery.ActivityLog do | |||||||
|   """ |   """ | ||||||
|  |  | ||||||
|   import Ecto.Query, warn: false |   import Ecto.Query, warn: false | ||||||
|   alias Cannery.Ammo.{AmmoGroup, AmmoType} |   alias Cannery.Ammo.{Pack, Type} | ||||||
|   alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo} |   alias Cannery.{Accounts.User, ActivityLog.ShotRecord, Repo} | ||||||
|   alias Ecto.{Multi, Queryable} |   alias Ecto.{Multi, Queryable} | ||||||
|  |  | ||||||
|  |   @type list_shot_records_option :: | ||||||
|  |           {:search, String.t() | nil} | ||||||
|  |           | {:class, Type.class() | :all | nil} | ||||||
|  |           | {:pack_id, Pack.id() | nil} | ||||||
|  |   @type list_shot_records_options :: [list_shot_records_option()] | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns the list of shot_groups. |   Returns the list of shot_records. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> list_shot_groups(:all, %User{id: 123}) |       iex> list_shot_records(%User{id: 123}) | ||||||
|       [%ShotGroup{}, ...] |       [%ShotRecord{}, ...] | ||||||
|  |  | ||||||
|       iex> list_shot_groups("cool", :all, %User{id: 123}) |       iex> list_shot_records(%User{id: 123}, search: "cool") | ||||||
|       [%ShotGroup{notes: "My cool shot group"}, ...] |       [%ShotRecord{notes: "My cool shot record"}, ...] | ||||||
|  |  | ||||||
|       iex> list_shot_groups("cool", :rifle, %User{id: 123}) |       iex> list_shot_records(%User{id: 123}, search: "cool", class: :rifle) | ||||||
|       [%ShotGroup{notes: "Shot some rifle rounds"}, ...] |       [%ShotRecord{notes: "Shot some rifle rounds"}, ...] | ||||||
|  |  | ||||||
|  |       iex> list_shot_records(%User{id: 123}, pack_id: 456) | ||||||
|  |       [%ShotRecord{pack_id: 456}, ...] | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec list_shot_groups(AmmoType.type() | :all, User.t()) :: [ShotGroup.t()] |   @spec list_shot_records(User.t()) :: [ShotRecord.t()] | ||||||
|   @spec list_shot_groups(search :: nil | String.t(), AmmoType.type() | :all, User.t()) :: |   @spec list_shot_records(User.t(), list_shot_records_options()) :: [ShotRecord.t()] | ||||||
|           [ShotGroup.t()] |   def list_shot_records(%User{id: user_id}, opts \\ []) do | ||||||
|   def list_shot_groups(search \\ nil, type, %{id: user_id}) do |     from(sr in ShotRecord, | ||||||
|     from(sg in ShotGroup, |       as: :sr, | ||||||
|       as: :sg, |       left_join: p in Pack, | ||||||
|       left_join: ag in AmmoGroup, |       as: :p, | ||||||
|       as: :ag, |       on: sr.pack_id == p.id, | ||||||
|       on: sg.ammo_group_id == ag.id, |       on: p.user_id == ^user_id, | ||||||
|       left_join: at in AmmoType, |       left_join: t in Type, | ||||||
|       as: :at, |       as: :t, | ||||||
|       on: ag.ammo_type_id == at.id, |       on: p.type_id == t.id, | ||||||
|       where: sg.user_id == ^user_id, |       on: t.user_id == ^user_id, | ||||||
|       distinct: sg.id |       where: sr.user_id == ^user_id, | ||||||
|  |       distinct: sr.id | ||||||
|     ) |     ) | ||||||
|     |> list_shot_groups_search(search) |     |> list_shot_records_search(Keyword.get(opts, :search)) | ||||||
|     |> list_shot_groups_filter_type(type) |     |> list_shot_records_class(Keyword.get(opts, :class)) | ||||||
|  |     |> list_shot_records_pack_id(Keyword.get(opts, :pack_id)) | ||||||
|     |> Repo.all() |     |> Repo.all() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec list_shot_groups_search(Queryable.t(), search :: String.t() | nil) :: |   @spec list_shot_records_search(Queryable.t(), search :: String.t() | nil) :: | ||||||
|           Queryable.t() |           Queryable.t() | ||||||
|   defp list_shot_groups_search(query, search) when search in ["", nil], do: query |   defp list_shot_records_search(query, search) when search in ["", nil], do: query | ||||||
|  |  | ||||||
|   defp list_shot_groups_search(query, search) when search |> is_binary() do |   defp list_shot_records_search(query, search) when search |> is_binary() do | ||||||
|     trimmed_search = String.trim(search) |     trimmed_search = String.trim(search) | ||||||
|  |  | ||||||
|     query |     query | ||||||
|     |> where( |     |> where( | ||||||
|       [sg: sg, ag: ag, at: at], |       [sr: sr, p: p, t: t], | ||||||
|       fragment( |       fragment( | ||||||
|         "? @@ websearch_to_tsquery('english', ?)", |         "? @@ websearch_to_tsquery('english', ?)", | ||||||
|         sg.search, |         sr.search, | ||||||
|         ^trimmed_search |         ^trimmed_search | ||||||
|       ) or |       ) or | ||||||
|         fragment( |         fragment( | ||||||
|           "? @@ websearch_to_tsquery('english', ?)", |           "? @@ websearch_to_tsquery('english', ?)", | ||||||
|           ag.search, |           p.search, | ||||||
|           ^trimmed_search |           ^trimmed_search | ||||||
|         ) or |         ) or | ||||||
|         fragment( |         fragment( | ||||||
|           "? @@ websearch_to_tsquery('english', ?)", |           "? @@ websearch_to_tsquery('english', ?)", | ||||||
|           at.search, |           t.search, | ||||||
|           ^trimmed_search |           ^trimmed_search | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|     |> order_by([sg: sg], { |     |> order_by([sr: sr], { | ||||||
|       :desc, |       :desc, | ||||||
|       fragment( |       fragment( | ||||||
|         "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", |         "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||||
|         sg.search, |         sr.search, | ||||||
|         ^trimmed_search |         ^trimmed_search | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec list_shot_groups_filter_type(Queryable.t(), AmmoType.type() | :all) :: |   @spec list_shot_records_class(Queryable.t(), Type.class() | :all | nil) :: Queryable.t() | ||||||
|           Queryable.t() |   defp list_shot_records_class(query, class) when class in [:rifle, :pistol, :shotgun], | ||||||
|   defp list_shot_groups_filter_type(query, :rifle), |     do: query |> where([t: t], t.class == ^class) | ||||||
|     do: query |> where([at: at], at.type == :rifle) |  | ||||||
|  |  | ||||||
|   defp list_shot_groups_filter_type(query, :pistol), |   defp list_shot_records_class(query, _all), do: query | ||||||
|     do: query |> where([at: at], at.type == :pistol) |  | ||||||
|  |  | ||||||
|   defp list_shot_groups_filter_type(query, :shotgun), |   @spec list_shot_records_pack_id(Queryable.t(), Pack.id() | nil) :: Queryable.t() | ||||||
|     do: query |> where([at: at], at.type == :shotgun) |   defp list_shot_records_pack_id(query, pack_id) when pack_id |> is_binary(), | ||||||
|  |     do: query |> where([sr: sr], sr.pack_id == ^pack_id) | ||||||
|  |  | ||||||
|   defp list_shot_groups_filter_type(query, _all), do: query |   defp list_shot_records_pack_id(query, _all), do: query | ||||||
|  |  | ||||||
|   @spec list_shot_groups_for_ammo_group(AmmoGroup.t(), User.t()) :: [ShotGroup.t()] |  | ||||||
|   def list_shot_groups_for_ammo_group( |  | ||||||
|         %AmmoGroup{id: ammo_group_id, user_id: user_id}, |  | ||||||
|         %User{id: user_id} |  | ||||||
|       ) do |  | ||||||
|     Repo.all( |  | ||||||
|       from sg in ShotGroup, |  | ||||||
|         where: sg.ammo_group_id == ^ammo_group_id, |  | ||||||
|         where: sg.user_id == ^user_id |  | ||||||
|     ) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Gets a single shot_group. |   Returns a count of shot records. | ||||||
|  |  | ||||||
|   Raises `Ecto.NoResultsError` if the Shot group does not exist. |  | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> get_shot_group!(123, %User{id: 123}) |       iex> get_shot_record_count!(%User{id: 123}) | ||||||
|       %ShotGroup{} |       3 | ||||||
|  |  | ||||||
|       iex> get_shot_group!(456, %User{id: 123}) |   """ | ||||||
|  |   @spec get_shot_record_count!(User.t()) :: integer() | ||||||
|  |   def get_shot_record_count!(%User{id: user_id}) do | ||||||
|  |     Repo.one( | ||||||
|  |       from sr in ShotRecord, | ||||||
|  |         where: sr.user_id == ^user_id, | ||||||
|  |         select: count(sr.id), | ||||||
|  |         distinct: true | ||||||
|  |     ) || 0 | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Gets a single shot_record. | ||||||
|  |  | ||||||
|  |   Raises `Ecto.NoResultsError` if the shot record does not exist. | ||||||
|  |  | ||||||
|  |   ## Examples | ||||||
|  |  | ||||||
|  |       iex> get_shot_record!(123, %User{id: 123}) | ||||||
|  |       %ShotRecord{} | ||||||
|  |  | ||||||
|  |       iex> get_shot_record!(456, %User{id: 123}) | ||||||
|       ** (Ecto.NoResultsError) |       ** (Ecto.NoResultsError) | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec get_shot_group!(ShotGroup.id(), User.t()) :: ShotGroup.t() |   @spec get_shot_record!(ShotRecord.id(), User.t()) :: ShotRecord.t() | ||||||
|   def get_shot_group!(id, %User{id: user_id}) do |   def get_shot_record!(id, %User{id: user_id}) do | ||||||
|     Repo.one!( |     Repo.one!( | ||||||
|       from sg in ShotGroup, |       from sr in ShotRecord, | ||||||
|         where: sg.id == ^id, |         where: sr.id == ^id, | ||||||
|         where: sg.user_id == ^user_id, |         where: sr.user_id == ^user_id, | ||||||
|         order_by: sg.date |         order_by: sr.date | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Creates a shot_group. |   Creates a shot_record. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> create_shot_group(%{field: value}, %User{id: 123}) |       iex> create_shot_record(%{field: value}, %User{id: 123}) | ||||||
|       {:ok, %ShotGroup{}} |       {:ok, %ShotRecord{}} | ||||||
|  |  | ||||||
|       iex> create_shot_group(%{field: bad_value}, %User{id: 123}) |       iex> create_shot_record(%{field: bad_value}, %User{id: 123}) | ||||||
|       {:error, %Ecto.Changeset{}} |       {:error, %Ecto.Changeset{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec create_shot_group(attrs :: map(), User.t(), AmmoGroup.t()) :: |   @spec create_shot_record(attrs :: map(), User.t(), Pack.t()) :: | ||||||
|           {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset() | nil} |           {:ok, ShotRecord.t()} | {:error, ShotRecord.changeset() | nil} | ||||||
|   def create_shot_group(attrs, user, ammo_group) do |   def create_shot_record(attrs, user, pack) do | ||||||
|     Multi.new() |     Multi.new() | ||||||
|     |> Multi.insert( |     |> Multi.insert( | ||||||
|       :create_shot_group, |       :create_shot_record, | ||||||
|       %ShotGroup{} |> ShotGroup.create_changeset(user, ammo_group, attrs) |       %ShotRecord{} |> ShotRecord.create_changeset(user, pack, attrs) | ||||||
|     ) |     ) | ||||||
|     |> Multi.run( |     |> Multi.run( | ||||||
|       :ammo_group, |       :pack, | ||||||
|       fn _repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} -> |       fn _repo, %{create_shot_record: %{pack_id: pack_id, user_id: user_id}} -> | ||||||
|         ammo_group = |         pack = | ||||||
|           Repo.one( |           Repo.one( | ||||||
|             from ag in AmmoGroup, |             from p in Pack, | ||||||
|               where: ag.id == ^ammo_group_id, |               where: p.id == ^pack_id, | ||||||
|               where: ag.user_id == ^user_id |               where: p.user_id == ^user_id | ||||||
|           ) |           ) | ||||||
|  |  | ||||||
|         {:ok, ammo_group} |         {:ok, pack} | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Multi.update( |     |> Multi.update( | ||||||
|       :update_ammo_group, |       :update_pack, | ||||||
|       fn %{create_shot_group: %{count: shot_group_count}, ammo_group: %{count: ammo_group_count}} -> |       fn %{create_shot_record: %{count: shot_record_count}, pack: %{count: pack_count}} -> | ||||||
|         ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count}) |         pack |> Pack.range_changeset(%{"count" => pack_count - shot_record_count}) | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Repo.transaction() |     |> Repo.transaction() | ||||||
|     |> case do |     |> case do | ||||||
|       {:ok, %{create_shot_group: shot_group}} -> {:ok, shot_group} |       {:ok, %{create_shot_record: shot_record}} -> {:ok, shot_record} | ||||||
|       {:error, :create_shot_group, changeset, _changes_so_far} -> {:error, changeset} |       {:error, :create_shot_record, changeset, _changes_so_far} -> {:error, changeset} | ||||||
|       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} |       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Updates a shot_group. |   Updates a shot_record. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> update_shot_group(shot_group, %{field: new_value}, %User{id: 123}) |       iex> update_shot_record(shot_record, %{field: new_value}, %User{id: 123}) | ||||||
|       {:ok, %ShotGroup{}} |       {:ok, %ShotRecord{}} | ||||||
|  |  | ||||||
|       iex> update_shot_group(shot_group, %{field: bad_value}, %User{id: 123}) |       iex> update_shot_record(shot_record, %{field: bad_value}, %User{id: 123}) | ||||||
|       {:error, %Ecto.Changeset{}} |       {:error, %Ecto.Changeset{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec update_shot_group(ShotGroup.t(), attrs :: map(), User.t()) :: |   @spec update_shot_record(ShotRecord.t(), attrs :: map(), User.t()) :: | ||||||
|           {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset() | nil} |           {:ok, ShotRecord.t()} | {:error, ShotRecord.changeset() | nil} | ||||||
|   def update_shot_group( |   def update_shot_record( | ||||||
|         %ShotGroup{count: count, user_id: user_id} = shot_group, |         %ShotRecord{count: count, user_id: user_id} = shot_record, | ||||||
|         attrs, |         attrs, | ||||||
|         %User{id: user_id} = user |         %User{id: user_id} = user | ||||||
|       ) do |       ) do | ||||||
|     Multi.new() |     Multi.new() | ||||||
|     |> Multi.update( |     |> Multi.update( | ||||||
|       :update_shot_group, |       :update_shot_record, | ||||||
|       shot_group |> ShotGroup.update_changeset(user, attrs) |       shot_record |> ShotRecord.update_changeset(user, attrs) | ||||||
|     ) |     ) | ||||||
|     |> Multi.run( |     |> Multi.run( | ||||||
|       :ammo_group, |       :pack, | ||||||
|       fn repo, %{update_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} -> |       fn repo, %{update_shot_record: %{pack_id: pack_id, user_id: user_id}} -> | ||||||
|         {:ok, |         {:ok, repo.one(from p in Pack, where: p.id == ^pack_id and p.user_id == ^user_id)} | ||||||
|          repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)} |  | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Multi.update( |     |> Multi.update( | ||||||
|       :update_ammo_group, |       :update_pack, | ||||||
|       fn %{ |       fn %{ | ||||||
|            update_shot_group: %{count: new_count}, |            update_shot_record: %{count: new_count}, | ||||||
|            ammo_group: %{count: ammo_group_count} = ammo_group |            pack: %{count: pack_count} = pack | ||||||
|          } -> |          } -> | ||||||
|         shot_diff_to_add = new_count - count |         shot_diff_to_add = new_count - count | ||||||
|         new_ammo_group_count = ammo_group_count - shot_diff_to_add |         new_pack_count = pack_count - shot_diff_to_add | ||||||
|         ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count}) |         pack |> Pack.range_changeset(%{"count" => new_pack_count}) | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Repo.transaction() |     |> Repo.transaction() | ||||||
|     |> case do |     |> case do | ||||||
|       {:ok, %{update_shot_group: shot_group}} -> {:ok, shot_group} |       {:ok, %{update_shot_record: shot_record}} -> {:ok, shot_record} | ||||||
|       {:error, :update_shot_group, changeset, _changes_so_far} -> {:error, changeset} |       {:error, :update_shot_record, changeset, _changes_so_far} -> {:error, changeset} | ||||||
|       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} |       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Deletes a shot_group. |   Deletes a shot_record. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> delete_shot_group(shot_group, %User{id: 123}) |       iex> delete_shot_record(shot_record, %User{id: 123}) | ||||||
|       {:ok, %ShotGroup{}} |       {:ok, %ShotRecord{}} | ||||||
|  |  | ||||||
|       iex> delete_shot_group(shot_group, %User{id: 123}) |       iex> delete_shot_record(shot_record, %User{id: 123}) | ||||||
|       {:error, %Ecto.Changeset{}} |       {:error, %Ecto.Changeset{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec delete_shot_group(ShotGroup.t(), User.t()) :: |   @spec delete_shot_record(ShotRecord.t(), User.t()) :: | ||||||
|           {:ok, ShotGroup.t()} | {:error, ShotGroup.changeset()} |           {:ok, ShotRecord.t()} | {:error, ShotRecord.changeset()} | ||||||
|   def delete_shot_group( |   def delete_shot_record( | ||||||
|         %ShotGroup{user_id: user_id} = shot_group, |         %ShotRecord{user_id: user_id} = shot_record, | ||||||
|         %User{id: user_id} |         %User{id: user_id} | ||||||
|       ) do |       ) do | ||||||
|     Multi.new() |     Multi.new() | ||||||
|     |> Multi.delete(:delete_shot_group, shot_group) |     |> Multi.delete(:delete_shot_record, shot_record) | ||||||
|     |> Multi.run( |     |> Multi.run( | ||||||
|       :ammo_group, |       :pack, | ||||||
|       fn repo, %{delete_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} -> |       fn repo, %{delete_shot_record: %{pack_id: pack_id, user_id: user_id}} -> | ||||||
|         {:ok, |         {:ok, repo.one(from p in Pack, where: p.id == ^pack_id and p.user_id == ^user_id)} | ||||||
|          repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)} |  | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Multi.update( |     |> Multi.update( | ||||||
|       :update_ammo_group, |       :update_pack, | ||||||
|       fn %{ |       fn %{ | ||||||
|            delete_shot_group: %{count: count}, |            delete_shot_record: %{count: count}, | ||||||
|            ammo_group: %{count: ammo_group_count} = ammo_group |            pack: %{count: pack_count} = pack | ||||||
|          } -> |          } -> | ||||||
|         new_ammo_group_count = ammo_group_count + count |         new_pack_count = pack_count + count | ||||||
|         ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count}) |         pack |> Pack.range_changeset(%{"count" => new_pack_count}) | ||||||
|       end |       end | ||||||
|     ) |     ) | ||||||
|     |> Repo.transaction() |     |> Repo.transaction() | ||||||
|     |> case do |     |> case do | ||||||
|       {:ok, %{delete_shot_group: shot_group}} -> {:ok, shot_group} |       {:ok, %{delete_shot_record: shot_record}} -> {:ok, shot_record} | ||||||
|       {:error, :delete_shot_group, changeset, _changes_so_far} -> {:error, changeset} |       {:error, :delete_shot_record, changeset, _changes_so_far} -> {:error, changeset} | ||||||
|       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} |       {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns the number of shot rounds for an ammo group |   Returns the last entered shot record date for a pack | ||||||
|   """ |   """ | ||||||
|   @spec get_used_count(AmmoGroup.t(), User.t()) :: non_neg_integer() |   @spec get_last_used_date(Pack.t(), User.t()) :: Date.t() | nil | ||||||
|   def get_used_count(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do |   def get_last_used_date(%Pack{id: pack_id} = pack, user) do | ||||||
|     [ammo_group] |     [pack] | ||||||
|     |> get_used_counts(user) |  | ||||||
|     |> Map.get(ammo_group_id, 0) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   @doc """ |  | ||||||
|   Returns the number of shot rounds for multiple ammo groups |  | ||||||
|   """ |  | ||||||
|   @spec get_used_counts([AmmoGroup.t()], User.t()) :: |  | ||||||
|           %{optional(AmmoGroup.id()) => non_neg_integer()} |  | ||||||
|   def get_used_counts(ammo_groups, %User{id: user_id}) do |  | ||||||
|     ammo_group_ids = |  | ||||||
|       ammo_groups |  | ||||||
|       |> Enum.map(fn %{id: ammo_group_id} -> ammo_group_id end) |  | ||||||
|  |  | ||||||
|     Repo.all( |  | ||||||
|       from sg in ShotGroup, |  | ||||||
|         where: sg.ammo_group_id in ^ammo_group_ids, |  | ||||||
|         where: sg.user_id == ^user_id, |  | ||||||
|         group_by: sg.ammo_group_id, |  | ||||||
|         select: {sg.ammo_group_id, sum(sg.count)} |  | ||||||
|     ) |  | ||||||
|     |> Map.new() |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   @doc """ |  | ||||||
|   Returns the last entered shot group date for an ammo group |  | ||||||
|   """ |  | ||||||
|   @spec get_last_used_date(AmmoGroup.t(), User.t()) :: Date.t() | nil |  | ||||||
|   def get_last_used_date(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do |  | ||||||
|     [ammo_group] |  | ||||||
|     |> get_last_used_dates(user) |     |> get_last_used_dates(user) | ||||||
|     |> Map.get(ammo_group_id) |     |> Map.get(pack_id) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns the last entered shot group date for an ammo group |   Returns the last entered shot record date for a pack | ||||||
|   """ |   """ | ||||||
|   @spec get_last_used_dates([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => Date.t()} |   @spec get_last_used_dates([Pack.t()], User.t()) :: %{optional(Pack.id()) => Date.t()} | ||||||
|   def get_last_used_dates(ammo_groups, %User{id: user_id}) do |   def get_last_used_dates(packs, %User{id: user_id}) do | ||||||
|     ammo_group_ids = |     pack_ids = | ||||||
|       ammo_groups |       packs | ||||||
|       |> Enum.map(fn %AmmoGroup{id: ammo_group_id, user_id: ^user_id} -> ammo_group_id end) |       |> Enum.map(fn %Pack{id: pack_id, user_id: ^user_id} -> pack_id end) | ||||||
|  |  | ||||||
|     Repo.all( |     Repo.all( | ||||||
|       from sg in ShotGroup, |       from sr in ShotRecord, | ||||||
|         where: sg.ammo_group_id in ^ammo_group_ids, |         where: sr.pack_id in ^pack_ids, | ||||||
|         where: sg.user_id == ^user_id, |         where: sr.user_id == ^user_id, | ||||||
|         group_by: sg.ammo_group_id, |         group_by: sr.pack_id, | ||||||
|         select: {sg.ammo_group_id, max(sg.date)} |         select: {sr.pack_id, max(sr.date)} | ||||||
|     ) |     ) | ||||||
|     |> Map.new() |     |> Map.new() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @type get_used_count_option :: {:pack_id, Pack.id() | nil} | {:type_id, Type.id() | nil} | ||||||
|   Gets the total number of rounds shot for an ammo type |   @type get_used_count_options :: [get_used_count_option()] | ||||||
|  |  | ||||||
|   Raises `Ecto.NoResultsError` if the Ammo type does not exist. |   @doc """ | ||||||
|  |   Gets the total number of rounds shot for a type | ||||||
|  |  | ||||||
|  |   Raises `Ecto.NoResultsError` if the type does not exist. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> get_used_count_for_ammo_type(123, %User{id: 123}) |       iex> get_used_count(%User{id: 123}, type_id: 123) | ||||||
|       35 |       35 | ||||||
|  |  | ||||||
|       iex> get_used_count_for_ammo_type(456, %User{id: 123}) |       iex> get_used_count(%User{id: 123}, pack_id: 456) | ||||||
|       ** (Ecto.NoResultsError) |       50 | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer() |   @spec get_used_count(User.t(), get_used_count_options()) :: non_neg_integer() | ||||||
|   def get_used_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do |   def get_used_count(%User{id: user_id}, opts) do | ||||||
|     [ammo_type] |     from(sr in ShotRecord, | ||||||
|     |> get_used_count_for_ammo_types(user) |       as: :sr, | ||||||
|     |> Map.get(ammo_type_id, 0) |       left_join: p in Pack, | ||||||
|  |       on: sr.pack_id == p.id, | ||||||
|  |       on: p.user_id == ^user_id, | ||||||
|  |       as: :p, | ||||||
|  |       where: sr.user_id == ^user_id, | ||||||
|  |       where: not (sr.count |> is_nil()), | ||||||
|  |       select: sum(sr.count), | ||||||
|  |       distinct: true | ||||||
|  |     ) | ||||||
|  |     |> get_used_count_type_id(Keyword.get(opts, :type_id)) | ||||||
|  |     |> get_used_count_pack_id(Keyword.get(opts, :pack_id)) | ||||||
|  |     |> Repo.one() || 0 | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   @spec get_used_count_pack_id(Queryable.t(), Pack.id() | nil) :: Queryable.t() | ||||||
|  |   defp get_used_count_pack_id(query, pack_id) when pack_id |> is_binary() do | ||||||
|  |     query |> where([sr: sr], sr.pack_id == ^pack_id) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_used_count_pack_id(query, _nil), do: query | ||||||
|  |  | ||||||
|  |   @spec get_used_count_type_id(Queryable.t(), Type.id() | nil) :: Queryable.t() | ||||||
|  |   defp get_used_count_type_id(query, type_id) when type_id |> is_binary() do | ||||||
|  |     query |> where([p: p], p.type_id == ^type_id) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_used_count_type_id(query, _nil), do: query | ||||||
|  |  | ||||||
|  |   @type get_grouped_used_counts_option :: | ||||||
|  |           {:packs, [Pack.t()] | nil} | ||||||
|  |           | {:types, [Type.t()] | nil} | ||||||
|  |           | {:group_by, :type_id | :pack_id} | ||||||
|  |   @type get_grouped_used_counts_options :: [get_grouped_used_counts_option()] | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Gets the total number of rounds shot for multiple ammo types |   Gets the total number of rounds shot for multiple types or packs | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> get_used_count_for_ammo_types(123, %User{id: 123}) |       iex> get_grouped_used_counts( | ||||||
|  |       ...>   %User{id: 123}, | ||||||
|  |       ...>   group_by: :type_id, | ||||||
|  |       ...>   types: [%Type{id: 456, user_id: 123}] | ||||||
|  |       ...> ) | ||||||
|       35 |       35 | ||||||
|  |  | ||||||
|   """ |       iex> get_grouped_used_counts( | ||||||
|   @spec get_used_count_for_ammo_types([AmmoType.t()], User.t()) :: |       ...>   %User{id: 123}, | ||||||
|           %{optional(AmmoType.id()) => non_neg_integer()} |       ...>   group_by: :pack_id, | ||||||
|   def get_used_count_for_ammo_types(ammo_types, %User{id: user_id}) do |       ...>   packs: [%Pack{id: 456, user_id: 123}] | ||||||
|     ammo_type_ids = |       ...> ) | ||||||
|       ammo_types |       22 | ||||||
|       |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) |  | ||||||
|  |  | ||||||
|     Repo.all( |   """ | ||||||
|       from ag in AmmoGroup, |   @spec get_grouped_used_counts(User.t(), get_grouped_used_counts_options()) :: | ||||||
|         left_join: sg in ShotGroup, |           %{optional(Type.id() | Pack.id()) => non_neg_integer()} | ||||||
|         on: ag.id == sg.ammo_group_id, |   def get_grouped_used_counts(%User{id: user_id}, opts) do | ||||||
|         where: ag.ammo_type_id in ^ammo_type_ids, |     from(p in Pack, | ||||||
|         where: not (sg.count |> is_nil()), |       as: :p, | ||||||
|         group_by: ag.ammo_type_id, |       left_join: sr in ShotRecord, | ||||||
|         select: {ag.ammo_type_id, sum(sg.count)} |       on: p.id == sr.pack_id, | ||||||
|  |       on: p.user_id == ^user_id, | ||||||
|  |       as: :sr, | ||||||
|  |       where: sr.user_id == ^user_id, | ||||||
|  |       where: not (sr.count |> is_nil()) | ||||||
|     ) |     ) | ||||||
|  |     |> get_grouped_used_counts_group_by(Keyword.fetch!(opts, :group_by)) | ||||||
|  |     |> get_grouped_used_counts_types(Keyword.get(opts, :types)) | ||||||
|  |     |> get_grouped_used_counts_packs(Keyword.get(opts, :packs)) | ||||||
|  |     |> Repo.all() | ||||||
|     |> Map.new() |     |> Map.new() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   @spec get_grouped_used_counts_group_by(Queryable.t(), :type_id | :pack_id) :: Queryable.t() | ||||||
|  |   defp get_grouped_used_counts_group_by(query, :type_id) do | ||||||
|  |     query | ||||||
|  |     |> group_by([p: p], p.type_id) | ||||||
|  |     |> select([sr: sr, p: p], {p.type_id, sum(sr.count)}) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_grouped_used_counts_group_by(query, :pack_id) do | ||||||
|  |     query | ||||||
|  |     |> group_by([sr: sr], sr.pack_id) | ||||||
|  |     |> select([sr: sr], {sr.pack_id, sum(sr.count)}) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @spec get_grouped_used_counts_types(Queryable.t(), [Type.t()] | nil) :: Queryable.t() | ||||||
|  |   defp get_grouped_used_counts_types(query, types) when types |> is_list() do | ||||||
|  |     type_ids = types |> Enum.map(fn %Type{id: type_id} -> type_id end) | ||||||
|  |     query |> where([p: p], p.type_id in ^type_ids) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_grouped_used_counts_types(query, _nil), do: query | ||||||
|  |  | ||||||
|  |   @spec get_grouped_used_counts_packs(Queryable.t(), [Pack.t()] | nil) :: Queryable.t() | ||||||
|  |   defp get_grouped_used_counts_packs(query, packs) when packs |> is_list() do | ||||||
|  |     pack_ids = packs |> Enum.map(fn %Pack{id: pack_id} -> pack_id end) | ||||||
|  |     query |> where([p: p], p.id in ^pack_ids) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_grouped_used_counts_packs(query, _nil), do: query | ||||||
| end | end | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| defmodule Cannery.ActivityLog.ShotGroup do | defmodule Cannery.ActivityLog.ShotRecord do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   A shot group records a group of ammo shot during a range trip |   A shot record records a group of ammo shot during a range trip | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use Ecto.Schema |   use Ecto.Schema | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   import Ecto.Changeset |   import Ecto.Changeset | ||||||
|   alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup} |   alias Cannery.{Accounts.User, Ammo, Ammo.Pack} | ||||||
|   alias Ecto.{Changeset, UUID} |   alias Ecto.{Changeset, UUID} | ||||||
| 
 | 
 | ||||||
|   @derive {Jason.Encoder, |   @derive {Jason.Encoder, | ||||||
| @@ -15,17 +15,17 @@ defmodule Cannery.ActivityLog.ShotGroup do | |||||||
|              :count, |              :count, | ||||||
|              :date, |              :date, | ||||||
|              :notes, |              :notes, | ||||||
|              :ammo_group_id |              :pack_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_records" do | ||||||
|     field :count, :integer |     field :count, :integer | ||||||
|     field :date, :date |     field :date, :date | ||||||
|     field :notes, :string |     field :notes, :string | ||||||
| 
 | 
 | ||||||
|     field :user_id, :binary_id |     field :user_id, :binary_id | ||||||
|     field :ammo_group_id, :binary_id |     field :pack_id, :binary_id | ||||||
| 
 | 
 | ||||||
|     timestamps() |     timestamps() | ||||||
|   end |   end | ||||||
| @@ -35,59 +35,57 @@ defmodule Cannery.ActivityLog.ShotGroup do | |||||||
|           count: integer, |           count: integer, | ||||||
|           notes: String.t() | nil, |           notes: String.t() | nil, | ||||||
|           date: Date.t() | nil, |           date: Date.t() | nil, | ||||||
|           ammo_group_id: AmmoGroup.id(), |           pack_id: Pack.id(), | ||||||
|           user_id: User.id(), |           user_id: User.id(), | ||||||
|           inserted_at: NaiveDateTime.t(), |           inserted_at: NaiveDateTime.t(), | ||||||
|           updated_at: NaiveDateTime.t() |           updated_at: NaiveDateTime.t() | ||||||
|         } |         } | ||||||
|   @type new_shot_group :: %__MODULE__{} |   @type new_shot_record :: %__MODULE__{} | ||||||
|   @type id :: UUID.t() |   @type id :: UUID.t() | ||||||
|   @type changeset :: Changeset.t(t() | new_shot_group()) |   @type changeset :: Changeset.t(t() | new_shot_record()) | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec create_changeset( |   @spec create_changeset( | ||||||
|           new_shot_group(), |           new_shot_record(), | ||||||
|           User.t() | any(), |           User.t() | any(), | ||||||
|           AmmoGroup.t() | any(), |           Pack.t() | any(), | ||||||
|           attrs :: map() |           attrs :: map() | ||||||
|         ) :: changeset() |         ) :: changeset() | ||||||
|   def create_changeset( |   def create_changeset( | ||||||
|         shot_group, |         shot_record, | ||||||
|         %User{id: user_id}, |         %User{id: user_id}, | ||||||
|         %AmmoGroup{id: ammo_group_id, user_id: user_id} = ammo_group, |         %Pack{id: pack_id, user_id: user_id} = pack, | ||||||
|         attrs |         attrs | ||||||
|       ) do |       ) do | ||||||
|     shot_group |     shot_record | ||||||
|     |> change(user_id: user_id) |     |> change(user_id: user_id) | ||||||
|     |> change(ammo_group_id: ammo_group_id) |     |> change(pack_id: pack_id) | ||||||
|     |> cast(attrs, [:count, :notes, :date]) |     |> cast(attrs, [:count, :notes, :date]) | ||||||
|     |> validate_length(:notes, max: 255) |     |> validate_length(:notes, max: 255) | ||||||
|     |> validate_create_shot_group_count(ammo_group) |     |> validate_create_shot_record_count(pack) | ||||||
|     |> validate_required([:date, :ammo_group_id, :user_id]) |     |> validate_required([:date, :pack_id, :user_id]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do |   def create_changeset(shot_record, _invalid_user, _invalid_pack, attrs) do | ||||||
|     shot_group |     shot_record | ||||||
|     |> cast(attrs, [:count, :notes, :date]) |     |> cast(attrs, [:count, :notes, :date]) | ||||||
|     |> validate_length(:notes, max: 255) |     |> validate_length(:notes, max: 255) | ||||||
|     |> validate_required([:ammo_group_id, :user_id]) |     |> validate_required([:pack_id, :user_id]) | ||||||
|     |> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo pack")) |     |> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo pack")) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp validate_create_shot_group_count(changeset, %AmmoGroup{count: ammo_group_count}) do |   defp validate_create_shot_record_count(changeset, %Pack{count: pack_count}) do | ||||||
|     case changeset |> Changeset.get_field(:count) do |     case changeset |> Changeset.get_field(:count) do | ||||||
|       nil -> |       nil -> | ||||||
|         changeset |> Changeset.add_error(:ammo_left, dgettext("errors", "can't be blank")) |         changeset |> Changeset.add_error(:ammo_left, dgettext("errors", "can't be blank")) | ||||||
| 
 | 
 | ||||||
|       count when count > ammo_group_count -> |       count when count > pack_count -> | ||||||
|         changeset |         changeset | ||||||
|         |> Changeset.add_error(:ammo_left, dgettext("errors", "Ammo left must be at least 0")) |         |> Changeset.add_error(:ammo_left, dgettext("errors", "Ammo left must be at least 0")) | ||||||
| 
 | 
 | ||||||
|       count when count <= 0 -> |       count when count <= 0 -> | ||||||
|         error = |         error = | ||||||
|           dgettext("errors", "Ammo left can be at most %{count} rounds", |           dgettext("errors", "Ammo left can be at most %{count} rounds", count: pack_count - 1) | ||||||
|             count: ammo_group_count - 1 |  | ||||||
|           ) |  | ||||||
| 
 | 
 | ||||||
|         changeset |> Changeset.add_error(:ammo_left, error) |         changeset |> Changeset.add_error(:ammo_left, error) | ||||||
| 
 | 
 | ||||||
| @@ -97,29 +95,28 @@ defmodule Cannery.ActivityLog.ShotGroup do | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec update_changeset(t() | new_shot_group(), User.t(), attrs :: map()) :: changeset() |   @spec update_changeset(t() | new_shot_record(), User.t(), attrs :: map()) :: changeset() | ||||||
|   def update_changeset(%__MODULE__{} = shot_group, user, attrs) do |   def update_changeset(%__MODULE__{} = shot_record, user, attrs) do | ||||||
|     shot_group |     shot_record | ||||||
|     |> cast(attrs, [:count, :notes, :date]) |     |> cast(attrs, [:count, :notes, :date]) | ||||||
|     |> validate_length(:notes, max: 255) |     |> validate_length(:notes, max: 255) | ||||||
|     |> validate_number(:count, greater_than: 0) |     |> validate_number(:count, greater_than: 0) | ||||||
|     |> validate_required([:count, :date]) |     |> validate_required([:count, :date]) | ||||||
|     |> validate_update_shot_group_count(shot_group, user) |     |> validate_update_shot_record_count(shot_record, user) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp validate_update_shot_group_count( |   defp validate_update_shot_record_count( | ||||||
|          changeset, |          changeset, | ||||||
|          %__MODULE__{ammo_group_id: ammo_group_id, count: count}, |          %__MODULE__{pack_id: pack_id, count: count}, | ||||||
|          user |          user | ||||||
|        ) do |        ) do | ||||||
|     %{count: ammo_group_count} = Ammo.get_ammo_group!(ammo_group_id, user) |     %{count: pack_count} = Ammo.get_pack!(pack_id, user) | ||||||
| 
 | 
 | ||||||
|     new_shot_group_count = changeset |> Changeset.get_field(:count) |     new_shot_record_count = changeset |> Changeset.get_field(:count) | ||||||
|     shot_diff_to_add = new_shot_group_count - count |     shot_diff_to_add = new_shot_record_count - count | ||||||
| 
 | 
 | ||||||
|     if shot_diff_to_add > ammo_group_count do |     if shot_diff_to_add > pack_count do | ||||||
|       error = |       error = dgettext("errors", "Count can be at most %{count} shots", count: pack_count + count) | ||||||
|         dgettext("errors", "Count can be at most %{count} shots", count: ammo_group_count + count) |  | ||||||
| 
 | 
 | ||||||
|       changeset |> Changeset.add_error(:count, error) |       changeset |> Changeset.add_error(:count, error) | ||||||
|     else |     else | ||||||
							
								
								
									
										1314
									
								
								lib/cannery/ammo.ex
									
									
									
									
									
								
							
							
						
						
									
										1314
									
								
								lib/cannery/ammo.ex
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| defmodule Cannery.Ammo.AmmoGroup do | defmodule Cannery.Ammo.Pack do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   A group of a certain ammunition type. |   A group of a certain ammunition type. | ||||||
| 
 | 
 | ||||||
| @@ -9,7 +9,7 @@ defmodule Cannery.Ammo.AmmoGroup do | |||||||
|   use Ecto.Schema |   use Ecto.Schema | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   import Ecto.Changeset |   import Ecto.Changeset | ||||||
|   alias Cannery.Ammo.AmmoType |   alias Cannery.Ammo.Type | ||||||
|   alias Cannery.{Accounts.User, Containers, Containers.Container} |   alias Cannery.{Accounts.User, Containers, Containers.Container} | ||||||
|   alias Ecto.{Changeset, UUID} |   alias Ecto.{Changeset, UUID} | ||||||
| 
 | 
 | ||||||
| @@ -19,20 +19,22 @@ defmodule Cannery.Ammo.AmmoGroup do | |||||||
|              :count, |              :count, | ||||||
|              :notes, |              :notes, | ||||||
|              :price_paid, |              :price_paid, | ||||||
|  |              :lot_number, | ||||||
|              :staged, |              :staged, | ||||||
|              :ammo_type_id, |              :type_id, | ||||||
|              :container_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 "packs" do | ||||||
|     field :count, :integer |     field :count, :integer | ||||||
|     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 :lot_number, :string | ||||||
|     field :purchased_on, :date |     field :purchased_on, :date | ||||||
| 
 | 
 | ||||||
|     belongs_to :ammo_type, AmmoType |     belongs_to :type, Type | ||||||
|     field :container_id, :binary_id |     field :container_id, :binary_id | ||||||
|     field :user_id, :binary_id |     field :user_id, :binary_id | ||||||
| 
 | 
 | ||||||
| @@ -45,60 +47,76 @@ 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(), | ||||||
|  |           lot_number: String.t() | nil, | ||||||
|           purchased_on: Date.t(), |           purchased_on: Date.t(), | ||||||
|           ammo_type: AmmoType.t() | nil, |           type: Type.t() | nil, | ||||||
|           ammo_type_id: AmmoType.id(), |           type_id: Type.id(), | ||||||
|           container_id: Container.id(), |           container_id: Container.id(), | ||||||
|           user_id: User.id(), |           user_id: User.id(), | ||||||
|           inserted_at: NaiveDateTime.t(), |           inserted_at: NaiveDateTime.t(), | ||||||
|           updated_at: NaiveDateTime.t() |           updated_at: NaiveDateTime.t() | ||||||
|         } |         } | ||||||
|   @type new_ammo_group :: %__MODULE__{} |   @type new_pack :: %__MODULE__{} | ||||||
|   @type id :: UUID.t() |   @type id :: UUID.t() | ||||||
|   @type changeset :: Changeset.t(t() | new_ammo_group()) |   @type changeset :: Changeset.t(t() | new_pack()) | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec create_changeset( |   @spec create_changeset( | ||||||
|           new_ammo_group(), |           new_pack(), | ||||||
|           AmmoType.t() | nil, |           Type.t() | nil, | ||||||
|           Container.t() | nil, |           Container.t() | nil, | ||||||
|           User.t(), |           User.t(), | ||||||
|           attrs :: map() |           attrs :: map() | ||||||
|         ) :: changeset() |         ) :: changeset() | ||||||
|   def create_changeset( |   def create_changeset( | ||||||
|         ammo_group, |         pack, | ||||||
|         %AmmoType{id: ammo_type_id}, |         %Type{id: type_id}, | ||||||
|         %Container{id: container_id, user_id: user_id}, |         %Container{id: container_id, user_id: user_id}, | ||||||
|         %User{id: user_id}, |         %User{id: user_id}, | ||||||
|         attrs |         attrs | ||||||
|       ) |       ) | ||||||
|       when is_binary(ammo_type_id) and is_binary(container_id) and is_binary(user_id) do |       when is_binary(type_id) and is_binary(container_id) and is_binary(user_id) do | ||||||
|     ammo_group |     pack | ||||||
|     |> change(ammo_type_id: ammo_type_id) |     |> change(type_id: type_id) | ||||||
|     |> change(user_id: user_id) |     |> change(user_id: user_id) | ||||||
|     |> change(container_id: container_id) |     |> change(container_id: container_id) | ||||||
|     |> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on]) |     |> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :lot_number]) | ||||||
|     |> validate_number(:count, greater_than: 0) |     |> validate_number(:count, greater_than: 0) | ||||||
|     |> validate_required([:count, :staged, :purchased_on, :ammo_type_id, :container_id, :user_id]) |     |> validate_number(:price_paid, greater_than_or_equal_to: 0) | ||||||
|  |     |> validate_length(:lot_number, max: 255) | ||||||
|  |     |> validate_required([:count, :staged, :purchased_on, :type_id, :container_id, :user_id]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @doc """ |   @doc """ | ||||||
|   Invalid changeset, used to prompt user to select ammo type and container |   Invalid changeset, used to prompt user to select type and container | ||||||
|   """ |   """ | ||||||
|   def create_changeset(ammo_group, _invalid_ammo_type, _invalid_container, _invalid_user, attrs) do |   def create_changeset(pack, _invalid_type, _invalid_container, _invalid_user, attrs) do | ||||||
|     ammo_group |     pack | ||||||
|     |> cast(attrs, [:ammo_type_id, :container_id]) |     |> cast(attrs, [:type_id, :container_id]) | ||||||
|     |> validate_required([:ammo_type_id, :container_id]) |     |> validate_required([:type_id, :container_id]) | ||||||
|     |> add_error(:invalid, dgettext("errors", "Please select an ammo type and container")) |     |> validate_number(:count, greater_than: 0) | ||||||
|  |     |> validate_number(:price_paid, greater_than_or_equal_to: 0) | ||||||
|  |     |> validate_length(:lot_number, max: 255) | ||||||
|  |     |> add_error(:invalid, dgettext("errors", "Please select a type and container")) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec update_changeset(t() | new_ammo_group(), attrs :: map(), User.t()) :: changeset() |   @spec update_changeset(t() | new_pack(), attrs :: map(), User.t()) :: changeset() | ||||||
|   def update_changeset(ammo_group, attrs, user) do |   def update_changeset(pack, attrs, user) do | ||||||
|     ammo_group |     pack | ||||||
|     |> cast(attrs, [:count, :price_paid, :notes, :staged, :purchased_on, :container_id]) |     |> cast(attrs, [ | ||||||
|  |       :count, | ||||||
|  |       :price_paid, | ||||||
|  |       :notes, | ||||||
|  |       :staged, | ||||||
|  |       :purchased_on, | ||||||
|  |       :lot_number, | ||||||
|  |       :container_id | ||||||
|  |     ]) | ||||||
|     |> validate_number(:count, greater_than_or_equal_to: 0) |     |> validate_number(:count, greater_than_or_equal_to: 0) | ||||||
|  |     |> validate_number(:price_paid, greater_than_or_equal_to: 0) | ||||||
|     |> validate_container_id(user) |     |> validate_container_id(user) | ||||||
|  |     |> validate_length(:lot_number, max: 255) | ||||||
|     |> validate_required([:count, :staged, :purchased_on, :container_id]) |     |> validate_required([:count, :staged, :purchased_on, :container_id]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -113,12 +131,12 @@ defmodule Cannery.Ammo.AmmoGroup do | |||||||
|   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" packs, and allows for | ||||||
|   updating the count to 0 |   updating the count to 0 | ||||||
|   """ |   """ | ||||||
|   @spec range_changeset(t() | new_ammo_group(), attrs :: map()) :: changeset() |   @spec range_changeset(t() | new_pack(), attrs :: map()) :: changeset() | ||||||
|   def range_changeset(ammo_group, attrs) do |   def range_changeset(pack, attrs) do | ||||||
|     ammo_group |     pack | ||||||
|     |> cast(attrs, [:count, :staged]) |     |> cast(attrs, [:count, :staged]) | ||||||
|     |> validate_required([:count, :staged]) |     |> validate_required([:count, :staged]) | ||||||
|   end |   end | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| defmodule Cannery.Ammo.AmmoType do | defmodule Cannery.Ammo.Type do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   An ammunition type. |   An ammunition type. | ||||||
| 
 | 
 | ||||||
| @@ -8,45 +8,53 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|   use Ecto.Schema |   use Ecto.Schema | ||||||
|   import Ecto.Changeset |   import Ecto.Changeset | ||||||
|   alias Cannery.Accounts.User |   alias Cannery.Accounts.User | ||||||
|   alias Cannery.Ammo.AmmoGroup |   alias Cannery.Ammo.Pack | ||||||
|   alias Ecto.{Changeset, UUID} |   alias Ecto.{Changeset, UUID} | ||||||
| 
 | 
 | ||||||
|   @derive {Jason.Encoder, |   @derive {Jason.Encoder, | ||||||
|            only: [ |            only: [ | ||||||
|              :id, |  | ||||||
|              :name, |              :name, | ||||||
|              :desc, |              :desc, | ||||||
|  |              :class, | ||||||
|              :bullet_type, |              :bullet_type, | ||||||
|              :bullet_core, |              :bullet_core, | ||||||
|              :cartridge, |  | ||||||
|              :caliber, |              :caliber, | ||||||
|              :case_material, |              :case_material, | ||||||
|              :jacket_type, |  | ||||||
|              :muzzle_velocity, |  | ||||||
|              :powder_type, |              :powder_type, | ||||||
|              :powder_grains_per_charge, |  | ||||||
|              :grains, |              :grains, | ||||||
|              :pressure, |              :pressure, | ||||||
|              :primer_type, |              :primer_type, | ||||||
|              :firing_type, |              :firing_type, | ||||||
|  |              :manufacturer, | ||||||
|  |              :upc, | ||||||
|              :tracer, |              :tracer, | ||||||
|              :incendiary, |              :incendiary, | ||||||
|              :blank, |              :blank, | ||||||
|              :corrosive, |              :corrosive, | ||||||
|              :manufacturer, |              :cartridge, | ||||||
|              :upc |              :jacket_type, | ||||||
|  |              :powder_grains_per_charge, | ||||||
|  |              :muzzle_velocity, | ||||||
|  |              :wadding, | ||||||
|  |              :shot_type, | ||||||
|  |              :shot_material, | ||||||
|  |              :shot_size, | ||||||
|  |              :unfired_length, | ||||||
|  |              :brass_height, | ||||||
|  |              :chamber_size, | ||||||
|  |              :load_grains, | ||||||
|  |              :shot_charge_weight, | ||||||
|  |              :dram_equivalent | ||||||
|            ]} |            ]} | ||||||
|   @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 "types" do | ||||||
|     field :name, :string |     field :name, :string | ||||||
|     field :desc, :string |     field :desc, :string | ||||||
| 
 | 
 | ||||||
|     field :type, Ecto.Enum, values: [:rifle, :shotgun, :pistol] |     field :class, Ecto.Enum, values: [:rifle, :shotgun, :pistol], default: :rifle | ||||||
| 
 | 
 | ||||||
|     # common fields |     # common fields | ||||||
|     # https://shootersreference.com/reloadingdata/bullet_abbreviations/ |  | ||||||
|     field :bullet_type, :string |  | ||||||
|     field :bullet_core, :string |     field :bullet_core, :string | ||||||
|     # also gauge for shotguns |     # also gauge for shotguns | ||||||
|     field :caliber, :string |     field :caliber, :string | ||||||
| @@ -65,6 +73,8 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|     field :corrosive, :boolean, default: false |     field :corrosive, :boolean, default: false | ||||||
| 
 | 
 | ||||||
|     # rifle/pistol fields |     # rifle/pistol fields | ||||||
|  |     # https://shootersreference.com/reloadingdata/bullet_abbreviations/ | ||||||
|  |     field :bullet_type, :string | ||||||
|     field :cartridge, :string |     field :cartridge, :string | ||||||
|     field :jacket_type, :string |     field :jacket_type, :string | ||||||
|     field :powder_grains_per_charge, :integer |     field :powder_grains_per_charge, :integer | ||||||
| @@ -83,7 +93,7 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|     field :dram_equivalent, :string |     field :dram_equivalent, :string | ||||||
| 
 | 
 | ||||||
|     field :user_id, :binary_id |     field :user_id, :binary_id | ||||||
|     has_many :ammo_groups, AmmoGroup |     has_many :packs, Pack | ||||||
| 
 | 
 | ||||||
|     timestamps() |     timestamps() | ||||||
|   end |   end | ||||||
| @@ -92,20 +102,26 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|           id: id(), |           id: id(), | ||||||
|           name: String.t(), |           name: String.t(), | ||||||
|           desc: String.t() | nil, |           desc: String.t() | nil, | ||||||
|           type: type(), |           class: class(), | ||||||
|           bullet_type: String.t() | nil, |           bullet_type: String.t() | nil, | ||||||
|           bullet_core: String.t() | nil, |           bullet_core: String.t() | nil, | ||||||
|           cartridge: String.t() | nil, |  | ||||||
|           caliber: String.t() | nil, |           caliber: String.t() | nil, | ||||||
|           case_material: String.t() | nil, |           case_material: String.t() | nil, | ||||||
|           jacket_type: String.t() | nil, |  | ||||||
|           muzzle_velocity: integer() | nil, |  | ||||||
|           powder_type: String.t() | nil, |           powder_type: String.t() | nil, | ||||||
|           powder_grains_per_charge: integer() | nil, |  | ||||||
|           grains: integer() | nil, |           grains: integer() | nil, | ||||||
|           pressure: String.t() | nil, |           pressure: String.t() | nil, | ||||||
|           primer_type: String.t() | nil, |           primer_type: String.t() | nil, | ||||||
|           firing_type: String.t() | nil, |           firing_type: String.t() | nil, | ||||||
|  |           manufacturer: String.t() | nil, | ||||||
|  |           upc: String.t() | nil, | ||||||
|  |           tracer: boolean(), | ||||||
|  |           incendiary: boolean(), | ||||||
|  |           blank: boolean(), | ||||||
|  |           corrosive: boolean(), | ||||||
|  |           cartridge: String.t() | nil, | ||||||
|  |           jacket_type: String.t() | nil, | ||||||
|  |           powder_grains_per_charge: integer() | nil, | ||||||
|  |           muzzle_velocity: integer() | nil, | ||||||
|           wadding: String.t() | nil, |           wadding: String.t() | nil, | ||||||
|           shot_type: String.t() | nil, |           shot_type: String.t() | nil, | ||||||
|           shot_material: String.t() | nil, |           shot_material: String.t() | nil, | ||||||
| @@ -116,41 +132,41 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|           load_grains: integer() | nil, |           load_grains: integer() | nil, | ||||||
|           shot_charge_weight: String.t() | nil, |           shot_charge_weight: String.t() | nil, | ||||||
|           dram_equivalent: String.t() | nil, |           dram_equivalent: String.t() | nil, | ||||||
|           tracer: boolean(), |  | ||||||
|           incendiary: boolean(), |  | ||||||
|           blank: boolean(), |  | ||||||
|           corrosive: boolean(), |  | ||||||
|           manufacturer: String.t() | nil, |  | ||||||
|           upc: String.t() | nil, |  | ||||||
|           user_id: User.id(), |           user_id: User.id(), | ||||||
|           ammo_groups: [AmmoGroup.t()] | nil, |           packs: [Pack.t()] | nil, | ||||||
|           inserted_at: NaiveDateTime.t(), |           inserted_at: NaiveDateTime.t(), | ||||||
|           updated_at: NaiveDateTime.t() |           updated_at: NaiveDateTime.t() | ||||||
|         } |         } | ||||||
|   @type new_ammo_type :: %__MODULE__{} |   @type new_type :: %__MODULE__{} | ||||||
|   @type id :: UUID.t() |   @type id :: UUID.t() | ||||||
|   @type changeset :: Changeset.t(t() | new_ammo_type()) |   @type changeset :: Changeset.t(t() | new_type()) | ||||||
|   @type type :: :rifle | :shotgun | :pistol | nil |   @type class :: :rifle | :shotgun | :pistol | nil | ||||||
| 
 | 
 | ||||||
|   @spec changeset_fields() :: [atom()] |   @spec changeset_fields() :: [atom()] | ||||||
|   defp changeset_fields, |   defp changeset_fields, | ||||||
|     do: [ |     do: [ | ||||||
|       :name, |       :name, | ||||||
|       :desc, |       :desc, | ||||||
|       :type, |       :class, | ||||||
|       :bullet_type, |       :bullet_type, | ||||||
|       :bullet_core, |       :bullet_core, | ||||||
|       :cartridge, |  | ||||||
|       :caliber, |       :caliber, | ||||||
|       :case_material, |       :case_material, | ||||||
|       :jacket_type, |  | ||||||
|       :muzzle_velocity, |  | ||||||
|       :powder_type, |       :powder_type, | ||||||
|       :powder_grains_per_charge, |  | ||||||
|       :grains, |       :grains, | ||||||
|       :pressure, |       :pressure, | ||||||
|       :primer_type, |       :primer_type, | ||||||
|       :firing_type, |       :firing_type, | ||||||
|  |       :manufacturer, | ||||||
|  |       :upc, | ||||||
|  |       :tracer, | ||||||
|  |       :incendiary, | ||||||
|  |       :blank, | ||||||
|  |       :corrosive, | ||||||
|  |       :cartridge, | ||||||
|  |       :jacket_type, | ||||||
|  |       :powder_grains_per_charge, | ||||||
|  |       :muzzle_velocity, | ||||||
|       :wadding, |       :wadding, | ||||||
|       :shot_type, |       :shot_type, | ||||||
|       :shot_material, |       :shot_material, | ||||||
| @@ -160,29 +176,26 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|       :chamber_size, |       :chamber_size, | ||||||
|       :load_grains, |       :load_grains, | ||||||
|       :shot_charge_weight, |       :shot_charge_weight, | ||||||
|       :dram_equivalent, |       :dram_equivalent | ||||||
|       :tracer, |  | ||||||
|       :incendiary, |  | ||||||
|       :blank, |  | ||||||
|       :corrosive, |  | ||||||
|       :manufacturer, |  | ||||||
|       :upc |  | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|   @spec string_fields() :: [atom()] |   @spec string_fields() :: [atom()] | ||||||
|   defp string_fields, |   defp string_fields, | ||||||
|     do: [ |     do: [ | ||||||
|       :name, |       :name, | ||||||
|  |       :desc, | ||||||
|       :bullet_type, |       :bullet_type, | ||||||
|       :bullet_core, |       :bullet_core, | ||||||
|       :cartridge, |  | ||||||
|       :caliber, |       :caliber, | ||||||
|       :case_material, |       :case_material, | ||||||
|       :jacket_type, |  | ||||||
|       :powder_type, |       :powder_type, | ||||||
|       :pressure, |       :pressure, | ||||||
|       :primer_type, |       :primer_type, | ||||||
|       :firing_type, |       :firing_type, | ||||||
|  |       :manufacturer, | ||||||
|  |       :upc, | ||||||
|  |       :cartridge, | ||||||
|  |       :jacket_type, | ||||||
|       :wadding, |       :wadding, | ||||||
|       :shot_type, |       :shot_type, | ||||||
|       :shot_material, |       :shot_material, | ||||||
| @@ -191,16 +204,14 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|       :brass_height, |       :brass_height, | ||||||
|       :chamber_size, |       :chamber_size, | ||||||
|       :shot_charge_weight, |       :shot_charge_weight, | ||||||
|       :dram_equivalent, |       :dram_equivalent | ||||||
|       :manufacturer, |  | ||||||
|       :upc |  | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec create_changeset(new_ammo_type(), User.t(), attrs :: map()) :: changeset() |   @spec create_changeset(new_type(), User.t(), attrs :: map()) :: changeset() | ||||||
|   def create_changeset(ammo_type, %User{id: user_id}, attrs) do |   def create_changeset(type, %User{id: user_id}, attrs) do | ||||||
|     changeset = |     changeset = | ||||||
|       ammo_type |       type | ||||||
|       |> change(user_id: user_id) |       |> change(user_id: user_id) | ||||||
|       |> cast(attrs, changeset_fields()) |       |> cast(attrs, changeset_fields()) | ||||||
| 
 | 
 | ||||||
| @@ -210,10 +221,10 @@ defmodule Cannery.Ammo.AmmoType do | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @doc false |   @doc false | ||||||
|   @spec update_changeset(t() | new_ammo_type(), attrs :: map()) :: changeset() |   @spec update_changeset(t() | new_type(), attrs :: map()) :: changeset() | ||||||
|   def update_changeset(ammo_type, attrs) do |   def update_changeset(type, attrs) do | ||||||
|     changeset = |     changeset = | ||||||
|       ammo_type |       type | ||||||
|       |> cast(attrs, changeset_fields()) |       |> cast(attrs, changeset_fields()) | ||||||
| 
 | 
 | ||||||
|     string_fields() |     string_fields() | ||||||
| @@ -5,12 +5,15 @@ defmodule Cannery.Containers do | |||||||
|  |  | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   import Ecto.Query, warn: false |   import Ecto.Query, warn: false | ||||||
|   alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo} |   alias Cannery.{Accounts.User, Ammo.Pack, Repo} | ||||||
|   alias Cannery.Containers.{Container, ContainerTag, Tag} |   alias Cannery.Containers.{Container, ContainerTag, Tag} | ||||||
|   alias Ecto.Changeset |   alias Ecto.{Changeset, Queryable} | ||||||
|  |  | ||||||
|   @container_preloads [:tags] |   @container_preloads [:tags] | ||||||
|  |  | ||||||
|  |   @type list_containers_option :: {:search, String.t() | nil} | ||||||
|  |   @type list_containers_options :: [list_containers_option()] | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns the list of containers. |   Returns the list of containers. | ||||||
|  |  | ||||||
| @@ -19,30 +22,31 @@ 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}) |       iex> list_containers(%User{id: 123}, search: "cool") | ||||||
|       [%Container{name: "my cool container"}, ...] |       [%Container{name: "my cool container"}, ...] | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec list_containers(User.t()) :: [Container.t()] |   @spec list_containers(User.t()) :: [Container.t()] | ||||||
|   @spec list_containers(search :: nil | String.t(), User.t()) :: [Container.t()] |   @spec list_containers(User.t(), list_containers_options()) :: [Container.t()] | ||||||
|   def list_containers(search \\ nil, %User{id: user_id}) do |   def list_containers(%User{id: user_id}, opts \\ []) do | ||||||
|     from(c in Container, |     from(c in Container, | ||||||
|       as: :c, |       as: :c, | ||||||
|       left_join: t in assoc(c, :tags), |       left_join: t in assoc(c, :tags), | ||||||
|  |       on: c.user_id == t.user_id, | ||||||
|       as: :t, |       as: :t, | ||||||
|       where: c.user_id == ^user_id, |       where: c.user_id == ^user_id, | ||||||
|       order_by: c.name, |  | ||||||
|       distinct: c.id, |       distinct: c.id, | ||||||
|       preload: ^@container_preloads |       preload: ^@container_preloads | ||||||
|     ) |     ) | ||||||
|     |> list_containers_search(search) |     |> list_containers_search(Keyword.get(opts, :search)) | ||||||
|     |> Repo.all() |     |> Repo.all() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp list_containers_search(query, nil), do: query |   @spec list_containers_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t() | ||||||
|   defp list_containers_search(query, ""), do: query |   defp list_containers_search(query, search) when search in ["", nil], | ||||||
|  |     do: query |> order_by([c: c], c.name) | ||||||
|  |  | ||||||
|   defp list_containers_search(query, search) do |   defp list_containers_search(query, search) when search |> is_binary() do | ||||||
|     trimmed_search = String.trim(search) |     trimmed_search = String.trim(search) | ||||||
|  |  | ||||||
|     query |     query | ||||||
| @@ -203,9 +207,9 @@ defmodule Cannery.Containers do | |||||||
|           {:ok, Container.t()} | {:error, Container.changeset()} |           {: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 p in Pack, | ||||||
|         where: ag.container_id == ^container.id, |         where: p.container_id == ^container.id, | ||||||
|         select: count(ag.id) |         select: count(p.id) | ||||||
|     ) |     ) | ||||||
|     |> case do |     |> case do | ||||||
|       0 -> |       0 -> | ||||||
| @@ -221,7 +225,7 @@ defmodule Cannery.Containers do | |||||||
|  |  | ||||||
|         container |         container | ||||||
|         |> Container.update_changeset(%{}) |         |> Container.update_changeset(%{}) | ||||||
|         |> Changeset.add_error(:ammo_groups, error) |         |> Changeset.add_error(:packs, error) | ||||||
|         |> Changeset.apply_action(:delete) |         |> Changeset.apply_action(:delete) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| @@ -289,6 +293,9 @@ defmodule Cannery.Containers do | |||||||
|  |  | ||||||
|   # Container Tags |   # Container Tags | ||||||
|  |  | ||||||
|  |   @type list_tags_option :: {:search, String.t() | nil} | ||||||
|  |   @type list_tags_options :: [list_tags_option()] | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns the list of tags. |   Returns the list of tags. | ||||||
|  |  | ||||||
| @@ -297,38 +304,42 @@ defmodule Cannery.Containers do | |||||||
|       iex> list_tags(%User{id: 123}) |       iex> list_tags(%User{id: 123}) | ||||||
|       [%Tag{}, ...] |       [%Tag{}, ...] | ||||||
|  |  | ||||||
|       iex> list_tags("cool", %User{id: 123}) |       iex> list_tags(%User{id: 123}, search: "cool") | ||||||
|       [%Tag{name: "my cool tag"}, ...] |       [%Tag{name: "my cool tag"}, ...] | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   @spec list_tags(User.t()) :: [Tag.t()] |   @spec list_tags(User.t()) :: [Tag.t()] | ||||||
|   @spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()] |   @spec list_tags(User.t(), list_tags_options()) :: [Tag.t()] | ||||||
|   def list_tags(search \\ nil, user) |   def list_tags(%User{id: user_id}, opts \\ []) do | ||||||
|  |     from(t in Tag, as: :t, where: t.user_id == ^user_id) | ||||||
|  |     |> list_tags_search(Keyword.get(opts, :search)) | ||||||
|  |     |> Repo.all() | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "", |   @spec list_tags_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t() | ||||||
|     do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name) |   defp list_tags_search(query, search) when search in ["", nil], | ||||||
|  |     do: query |> order_by([t: t], t.name) | ||||||
|  |  | ||||||
|   def list_tags(search, %{id: user_id}) when search |> is_binary() do |   defp list_tags_search(query, search) when search |> is_binary() do | ||||||
|     trimmed_search = String.trim(search) |     trimmed_search = String.trim(search) | ||||||
|  |  | ||||||
|     Repo.all( |     query | ||||||
|       from t in Tag, |     |> where( | ||||||
|         where: t.user_id == ^user_id, |       [t: t], | ||||||
|         where: |  | ||||||
|       fragment( |       fragment( | ||||||
|         "? @@ websearch_to_tsquery('english', ?)", |         "? @@ websearch_to_tsquery('english', ?)", | ||||||
|         t.search, |         t.search, | ||||||
|         ^trimmed_search |         ^trimmed_search | ||||||
|           ), |       ) | ||||||
|         order_by: { |     ) | ||||||
|  |     |> order_by([t: t], { | ||||||
|       :desc, |       :desc, | ||||||
|       fragment( |       fragment( | ||||||
|         "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", |         "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", | ||||||
|         t.search, |         t.search, | ||||||
|         ^trimmed_search |         ^trimmed_search | ||||||
|       ) |       ) | ||||||
|         } |     }) | ||||||
|     ) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   | |||||||
| @@ -14,17 +14,17 @@ defmodule Cannery.Logger do | |||||||
|       |> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace)) |       |> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace)) | ||||||
|       |> pretty_encode() |       |> pretty_encode() | ||||||
|  |  | ||||||
|     Logger.error(meta.reason, data: data) |     Logger.error("Oban exception: #{data}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event([:oban, :job, :start], measure, meta, _config) do |   def handle_event([:oban, :job, :start], measure, meta, _config) do | ||||||
|     data = get_oban_job_data(meta, measure) |> pretty_encode() |     data = get_oban_job_data(meta, measure) |> pretty_encode() | ||||||
|     Logger.info("Started oban job", data: data) |     Logger.info("Started oban job: #{data}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event([:oban, :job, :stop], measure, meta, _config) do |   def handle_event([:oban, :job, :stop], measure, meta, _config) do | ||||||
|     data = get_oban_job_data(meta, measure) |> pretty_encode() |     data = get_oban_job_data(meta, measure) |> pretty_encode() | ||||||
|     Logger.info("Finished oban job", data: data) |     Logger.info("Finished oban job: #{data}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do |   def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do | ||||||
| @@ -33,7 +33,7 @@ defmodule Cannery.Logger do | |||||||
|       |> Map.put(:event, unhandled_event) |       |> Map.put(:event, unhandled_event) | ||||||
|       |> pretty_encode() |       |> pretty_encode() | ||||||
|  |  | ||||||
|     Logger.warning("Unhandled oban job event", data: data) |     Logger.warning("Unhandled oban job event: #{data}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event(unhandled_event, measure, meta, config) do |   def handle_event(unhandled_event, measure, meta, config) do | ||||||
| @@ -45,7 +45,7 @@ defmodule Cannery.Logger do | |||||||
|         config: config |         config: config | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|     Logger.warning("Unhandled telemetry event", data: data) |     Logger.warning("Unhandled telemetry event: #{data}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp get_oban_job_data(%{job: job}, measure) do |   defp get_oban_job_data(%{job: job}, measure) do | ||||||
|   | |||||||
| @@ -1,53 +1,61 @@ | |||||||
| defmodule CanneryWeb do | defmodule CanneryWeb do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   The entrypoint for defining your web interface, such |   The entrypoint for defining your web interface, such | ||||||
|   as controllers, views, channels and so on. |   as controllers, components, channels, and so on. | ||||||
|  |  | ||||||
|   This can be used in your application as: |   This can be used in your application as: | ||||||
|  |  | ||||||
|       use CanneryWeb, :controller |       use CanneryWeb, :controller | ||||||
|       use CanneryWeb, :view |       use CanneryWeb, :html | ||||||
|  |  | ||||||
|   The definitions below will be executed for every view, |   The definitions below will be executed for every controller, | ||||||
|   controller, etc, so keep them short and clean, focused |   component, etc, so keep them short and clean, focused | ||||||
|   on imports, uses and aliases. |   on imports, uses and aliases. | ||||||
|  |  | ||||||
|   Do NOT define functions inside the quoted expressions |   Do NOT define functions inside the quoted expressions | ||||||
|   below. Instead, define any helper function in modules |   below. Instead, define additional modules and import | ||||||
|   and import those modules here. |   those modules here. | ||||||
|   """ |   """ | ||||||
|  |  | ||||||
|  |   def static_paths, do: ~w(css js fonts images favicon.ico robots.txt) | ||||||
|  |  | ||||||
|  |   def router do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.Router, helpers: false | ||||||
|  |  | ||||||
|  |       # Import common connection and controller functions to use in pipelines | ||||||
|  |       import Plug.Conn | ||||||
|  |       import Phoenix.Controller | ||||||
|  |       import Phoenix.LiveView.Router | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def channel do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.Channel | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def controller do |   def controller do | ||||||
|     quote do |     quote do | ||||||
|       use Phoenix.Controller, namespace: CanneryWeb |       use Phoenix.Controller, | ||||||
|  |         formats: [:html, :json], | ||||||
|  |         layouts: [html: CanneryWeb.Layouts] | ||||||
|  |  | ||||||
|  |       # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse | ||||||
|       import Plug.Conn |       import Plug.Conn | ||||||
|       import CanneryWeb.Gettext |       import CanneryWeb.Gettext | ||||||
|       alias CanneryWeb.Router.Helpers, as: Routes |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def view do |       unquote(verified_routes()) | ||||||
|     quote do |  | ||||||
|       use Phoenix.View, |  | ||||||
|         root: "lib/cannery_web/templates", |  | ||||||
|         namespace: CanneryWeb |  | ||||||
|  |  | ||||||
|       # Import convenience functions from controllers |  | ||||||
|       import Phoenix.Controller, |  | ||||||
|         only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] |  | ||||||
|  |  | ||||||
|       # Include shared imports and aliases for views |  | ||||||
|       unquote(view_helpers()) |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def live_view do |   def live_view do | ||||||
|     quote do |     quote do | ||||||
|       use Phoenix.LiveView, layout: {CanneryWeb.LayoutView, :live} |       use Phoenix.LiveView, | ||||||
|  |         layout: {CanneryWeb.Layouts, :app} | ||||||
|  |  | ||||||
|       on_mount CanneryWeb.InitAssigns |       unquote(html_helpers()) | ||||||
|       unquote(view_helpers()) |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -55,49 +63,44 @@ defmodule CanneryWeb do | |||||||
|     quote do |     quote do | ||||||
|       use Phoenix.LiveComponent |       use Phoenix.LiveComponent | ||||||
|  |  | ||||||
|       unquote(view_helpers()) |       unquote(html_helpers()) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def component do |   def html do | ||||||
|     quote do |     quote do | ||||||
|  |       # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse | ||||||
|       use Phoenix.Component |       use Phoenix.Component | ||||||
|  |  | ||||||
|       unquote(view_helpers()) |       # Import convenience functions from controllers | ||||||
|  |       import Phoenix.Controller, | ||||||
|  |         only: [get_csrf_token: 0, view_module: 1, view_template: 1] | ||||||
|  |  | ||||||
|  |       # Include general helpers for rendering HTML | ||||||
|  |       unquote(html_helpers()) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def router do |   defp html_helpers do | ||||||
|     quote do |     quote do | ||||||
|       use Phoenix.Router |       use PhoenixHTMLHelpers | ||||||
|  |       import Phoenix.{Component, HTML, HTML.Form} | ||||||
|  |       import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers} | ||||||
|  |  | ||||||
|       import Phoenix.{Controller, LiveView.Router} |       # Shortcut for generating JS commands | ||||||
|       # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse |       alias Phoenix.LiveView.JS | ||||||
|       import Plug.Conn |  | ||||||
|  |       # Routes generation with the ~p sigil | ||||||
|  |       unquote(verified_routes()) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def channel do |   def verified_routes do | ||||||
|     quote do |     quote do | ||||||
|       use Phoenix.Channel |       use Phoenix.VerifiedRoutes, | ||||||
|       # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse |         endpoint: CanneryWeb.Endpoint, | ||||||
|       import CanneryWeb.Gettext |         router: CanneryWeb.Router, | ||||||
|     end |         statics: CanneryWeb.static_paths() | ||||||
|   end |  | ||||||
|  |  | ||||||
|   defp view_helpers do |  | ||||||
|     quote do |  | ||||||
|       # Use all HTML functionality (forms, tags, etc) |  | ||||||
|       # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse |  | ||||||
|       use Phoenix.HTML |  | ||||||
|  |  | ||||||
|       # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) |  | ||||||
|       # Import basic rendering functionality (render, render_layout, etc) |  | ||||||
|       import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers} |  | ||||||
|       import Phoenix.{Component, View} |  | ||||||
|  |  | ||||||
|       alias CanneryWeb.Endpoint |  | ||||||
|       alias CanneryWeb.Router.Helpers, as: Routes |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| defmodule CanneryWeb.Components.AddShotGroupComponent do | defmodule CanneryWeb.Components.AddShotRecordComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Livecomponent that can create a ShotGroup |   Livecomponent that can create a ShotRecord | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup} |   alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotRecord, Ammo.Pack} | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|   alias Phoenix.LiveView.{JS, Socket} |   alias Phoenix.LiveView.{JS, Socket} | ||||||
| 
 | 
 | ||||||
| @@ -12,15 +12,15 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do | |||||||
|   @spec update( |   @spec update( | ||||||
|           %{ |           %{ | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             required(:ammo_group) => AmmoGroup.t(), |             required(:pack) => Pack.t(), | ||||||
|             optional(any()) => any() |             optional(any()) => any() | ||||||
|           }, |           }, | ||||||
|           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(%{pack: pack, current_user: current_user} = assigns, socket) do | ||||||
|     changeset = |     changeset = | ||||||
|       %ShotGroup{date: Date.utc_today()} |       %ShotRecord{date: Date.utc_today()} | ||||||
|       |> ShotGroup.create_changeset(current_user, ammo_group, %{}) |       |> ShotRecord.create_changeset(current_user, pack, %{}) | ||||||
| 
 | 
 | ||||||
|     {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)} |     {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)} | ||||||
|   end |   end | ||||||
| @@ -28,12 +28,12 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do | |||||||
|   @impl true |   @impl true | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "validate", |         "validate", | ||||||
|         %{"shot_group" => shot_group_params}, |         %{"shot_record" => shot_record_params}, | ||||||
|         %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket |         %{assigns: %{pack: pack, current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     params = shot_group_params |> process_params(ammo_group) |     params = shot_record_params |> process_params(pack) | ||||||
| 
 | 
 | ||||||
|     changeset = %ShotGroup{} |> ShotGroup.create_changeset(current_user, ammo_group, params) |     changeset = %ShotRecord{} |> ShotRecord.create_changeset(current_user, pack, params) | ||||||
| 
 | 
 | ||||||
|     changeset = |     changeset = | ||||||
|       case changeset |> Changeset.apply_action(:validate) do |       case changeset |> Changeset.apply_action(:validate) do | ||||||
| @@ -46,17 +46,17 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do | |||||||
| 
 | 
 | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "save", |         "save", | ||||||
|         %{"shot_group" => shot_group_params}, |         %{"shot_record" => shot_record_params}, | ||||||
|         %{ |         %{ | ||||||
|           assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to} |           assigns: %{pack: pack, current_user: current_user, return_to: return_to} | ||||||
|         } = socket |         } = socket | ||||||
|       ) do |       ) do | ||||||
|     socket = |     socket = | ||||||
|       shot_group_params |       shot_record_params | ||||||
|       |> process_params(ammo_group) |       |> process_params(pack) | ||||||
|       |> ActivityLog.create_shot_group(current_user, ammo_group) |       |> ActivityLog.create_shot_record(current_user, pack) | ||||||
|       |> case do |       |> case do | ||||||
|         {:ok, _shot_group} -> |         {:ok, _shot_record} -> | ||||||
|           prompt = dgettext("prompts", "Shots recorded successfully") |           prompt = dgettext("prompts", "Shots recorded successfully") | ||||||
|           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) |           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) | ||||||
| 
 | 
 | ||||||
| @@ -68,8 +68,8 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # calculate count from shots left |   # calculate count from shots left | ||||||
|   defp process_params(params, %AmmoGroup{count: count}) do |   defp process_params(params, %Pack{count: count}) do | ||||||
|     shot_group_count = |     shot_record_count = | ||||||
|       if params |> Map.get("ammo_left", "") == "" do |       if params |> Map.get("ammo_left", "") == "" do | ||||||
|         nil |         nil | ||||||
|       else |       else | ||||||
| @@ -77,6 +77,6 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do | |||||||
|         count - new_count |         count - new_count | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|     params |> Map.put("count", shot_group_count) |     params |> Map.put("count", shot_record_count) | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @@ -6,14 +6,14 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@changeset} |     for={@changeset} | ||||||
|     id="shot-group-form" |     id="shot-record-form" | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|     phx-target={@myself} |     phx-target={@myself} | ||||||
|     phx-change="validate" |     phx-change="validate" | ||||||
|     phx-submit="save" |     phx-submit="save" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       :if={@changeset.action && not @changeset.valid?()} |       :if={@changeset.action && not @changeset.valid?} | ||||||
|       class="invalid-feedback col-span-3 text-center" |       class="invalid-feedback col-span-3 text-center" | ||||||
|     > |     > | ||||||
|       <%= changeset_errors(@changeset) %> |       <%= changeset_errors(@changeset) %> | ||||||
| @@ -22,14 +22,14 @@ | |||||||
|     <%= label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600") %> |     <%= label(f, :ammo_left, gettext("Rounds left"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= number_input(f, :ammo_left, |     <%= number_input(f, :ammo_left, | ||||||
|       min: 0, |       min: 0, | ||||||
|       max: @ammo_group.count - 1, |       max: @pack.count - 1, | ||||||
|       placeholder: gettext("Rounds left"), |       placeholder: gettext("Rounds left"), | ||||||
|       class: "input input-primary" |       class: "input input-primary" | ||||||
|     ) %> |     ) %> | ||||||
|     <button |     <button | ||||||
|       type="button" |       type="button" | ||||||
|       class="mx-2 my-1 text-sm btn btn-primary" |       class="mx-2 my-1 text-sm btn btn-primary" | ||||||
|       phx-click={JS.dispatch("cannery:set-zero", to: "#shot-group-form_ammo_left")} |       phx-click={JS.dispatch("cannery:set-zero", to: "#shot-record-form_ammo_left")} | ||||||
|     > |     > | ||||||
|       <%= gettext("Used up!") %> |       <%= gettext("Used up!") %> | ||||||
|     </button> |     </button> | ||||||
| @@ -37,11 +37,10 @@ | |||||||
| 
 | 
 | ||||||
|     <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %> |     <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= textarea(f, :notes, |     <%= textarea(f, :notes, | ||||||
|       id: "add-shot-group-form-notes", |       id: "add-shot-record-form-notes", | ||||||
|       class: "input input-primary col-span-2", |       class: "input input-primary col-span-2", | ||||||
|       maxlength: 255, |       maxlength: 255, | ||||||
|       placeholder: gettext("Really great weather"), |       placeholder: gettext("Really great weather"), | ||||||
|       phx_hook: "MaintainAttrs", |  | ||||||
|       phx_update: "ignore" |       phx_update: "ignore" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :notes, "col-span-3") %> |     <%= error_tag(f, :notes, "col-span-3") %> | ||||||
| @@ -71,8 +71,16 @@ defmodule CanneryWeb.Components.ContainerTableComponent do | |||||||
|       current_user: current_user, |       current_user: current_user, | ||||||
|       tag_actions: tag_actions, |       tag_actions: tag_actions, | ||||||
|       actions: actions, |       actions: actions, | ||||||
|       pack_count: Ammo.get_ammo_groups_count_for_containers(containers, current_user), |       pack_count: | ||||||
|       round_count: Ammo.get_round_count_for_containers(containers, current_user) |         Ammo.get_grouped_packs_count(current_user, | ||||||
|  |           containers: containers, | ||||||
|  |           group_by: :container_id | ||||||
|  |         ), | ||||||
|  |       round_count: | ||||||
|  |         Ammo.get_grouped_round_count(current_user, | ||||||
|  |           containers: containers, | ||||||
|  |           group_by: :container_id | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     rows = |     rows = | ||||||
| @@ -109,14 +117,12 @@ defmodule CanneryWeb.Components.ContainerTableComponent do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec get_value_for_key(atom(), Container.t(), extra_data :: map) :: any() |   @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 |   defp get_value_for_key(:name, %{name: container_name} = assigns, _extra_data) do | ||||||
|     assigns = %{id: id, container_name: container_name} |  | ||||||
|  |  | ||||||
|     {container_name, |     {container_name, | ||||||
|      ~H""" |      ~H""" | ||||||
|      <div class="flex flex-wrap justify-center items-center"> |      <div class="flex flex-wrap justify-center items-center"> | ||||||
|        <.link navigate={Routes.container_show_path(Endpoint, :show, @id)} class="link"> |        <.link navigate={~p"/container/#{@id}"} class="link"> | ||||||
|          <%= @container_name %> |          <%= @name %> | ||||||
|        </.link> |        </.link> | ||||||
|      </div> |      </div> | ||||||
|      """} |      """} | ||||||
|   | |||||||
| @@ -3,11 +3,11 @@ defmodule CanneryWeb.CoreComponents do | |||||||
|   Provides core UI components. |   Provides core UI components. | ||||||
|   """ |   """ | ||||||
|   use Phoenix.Component |   use Phoenix.Component | ||||||
|   import CanneryWeb.{Gettext, ViewHelpers} |   use CanneryWeb, :verified_routes | ||||||
|  |   import CanneryWeb.{Gettext, HTMLHelpers} | ||||||
|   alias Cannery.{Accounts, Accounts.Invite, Accounts.User} |   alias Cannery.{Accounts, Accounts.Invite, Accounts.User} | ||||||
|   alias Cannery.{Ammo, Ammo.AmmoGroup} |   alias Cannery.{Ammo, Ammo.Pack} | ||||||
|   alias Cannery.{Containers.Container, Containers.Tag} |   alias Cannery.{Containers.Container, Containers.Tag} | ||||||
|   alias CanneryWeb.{Endpoint, HomeLive} |  | ||||||
|   alias CanneryWeb.Router.Helpers, as: Routes |   alias CanneryWeb.Router.Helpers, as: Routes | ||||||
|   alias Phoenix.LiveView.{JS, Rendered} |   alias Phoenix.LiveView.{JS, Rendered} | ||||||
|  |  | ||||||
| @@ -29,13 +29,13 @@ defmodule CanneryWeb.CoreComponents do | |||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       <.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}> |       <.modal return_to={~p"/\#{<%= schema.plural %>}"}> | ||||||
|         <.live_component |         <.live_component | ||||||
|           module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent} |           module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent} | ||||||
|           id={@<%= schema.singular %>.id || :new} |           id={@<%= schema.singular %>.id || :new} | ||||||
|           title={@page_title} |           title={@page_title} | ||||||
|           action={@live_action} |           action={@live_action} | ||||||
|           return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)} |           return_to={~p"/\#{<%= schema.singular %>}"} | ||||||
|           <%= schema.singular %>: @<%= schema.singular %> |           <%= schema.singular %>: @<%= schema.singular %> | ||||||
|         /> |         /> | ||||||
|       </.modal> |       </.modal> | ||||||
| @@ -86,7 +86,7 @@ defmodule CanneryWeb.CoreComponents do | |||||||
|  |  | ||||||
|   def simple_tag_card(assigns) |   def simple_tag_card(assigns) | ||||||
|  |  | ||||||
|   attr :ammo_group, AmmoGroup, required: true |   attr :pack, Pack, required: true | ||||||
|   attr :current_user, User, required: true |   attr :current_user, User, required: true | ||||||
|   attr :original_count, :integer, default: nil |   attr :original_count, :integer, default: nil | ||||||
|   attr :cpr, :integer, default: nil |   attr :cpr, :integer, default: nil | ||||||
| @@ -94,7 +94,7 @@ defmodule CanneryWeb.CoreComponents do | |||||||
|   attr :container, Container, default: nil |   attr :container, Container, default: nil | ||||||
|   slot(:inner_block) |   slot(:inner_block) | ||||||
|  |  | ||||||
|   def ammo_group_card(assigns) |   def pack_card(assigns) | ||||||
|  |  | ||||||
|   @spec display_currency(float()) :: String.t() |   @spec display_currency(float()) :: String.t() | ||||||
|   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) |   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|     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" | ||||||
| > | > | ||||||
|   <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link"> |   <.link navigate={~p"/container/#{@container}"} class="link"> | ||||||
|     <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> | ||||||
| @@ -27,15 +27,15 @@ | |||||||
|       <%= @container.location %> |       <%= @container.location %> | ||||||
|     </span> |     </span> | ||||||
|  |  | ||||||
|     <%= if @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) != 0 do %> |     <%= if Ammo.get_packs_count(@current_user, container_id: @container.id) != 0 do %> | ||||||
|       <span class="rounded-lg title text-lg"> |       <span class="rounded-lg title text-lg"> | ||||||
|         <%= gettext("Packs:") %> |         <%= gettext("Packs:") %> | ||||||
|         <%= @container |> Ammo.get_ammo_groups_count_for_container!(@current_user) %> |         <%= Ammo.get_packs_count(@current_user, container_id: @container.id) %> | ||||||
|       </span> |       </span> | ||||||
|  |  | ||||||
|       <span class="rounded-lg title text-lg"> |       <span class="rounded-lg title text-lg"> | ||||||
|         <%= gettext("Rounds:") %> |         <%= gettext("Rounds:") %> | ||||||
|         <%= @container |> Ammo.get_round_count_for_container!(@current_user) %> |         <%= Ammo.get_round_count(@current_user, container_id: @container.id) %> | ||||||
|       </span> |       </span> | ||||||
|     <% end %> |     <% end %> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ | |||||||
|   <% end %> |   <% end %> | ||||||
|  |  | ||||||
|   <.qr_code |   <.qr_code | ||||||
|     content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)} |     content={url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")} | ||||||
|     filename={@invite.name} |     filename={@invite.name} | ||||||
|   /> |   /> | ||||||
|  |  | ||||||
| @@ -36,7 +36,7 @@ | |||||||
|       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 |       phx-no-format | ||||||
|     ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code> |     ><%= url(CanneryWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code> | ||||||
|     <%= if @code_actions, do: render_slot(@code_actions) %> |     <%= if @code_actions, do: render_slot(@code_actions) %> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,48 +1,45 @@ | |||||||
| <div | <div | ||||||
|   id={"ammo_group-#{@ammo_group.id}"} |   id={"pack-#{@pack.id}"} | ||||||
|   class="mx-4 my-2 px-8 py-4 |   class="mx-4 my-2 px-8 py-4 | ||||||
|     flex flex-col justify-center items-center |     flex flex-col 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" | ||||||
| > | > | ||||||
|   <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link"> |   <.link navigate={~p"/ammo/show/#{@pack}"} class="mb-2 link"> | ||||||
|     <h1 class="title text-xl title-primary-500"> |     <h1 class="title text-xl title-primary-500"> | ||||||
|       <%= @ammo_group.ammo_type.name %> |       <%= @pack.type.name %> | ||||||
|     </h1> |     </h1> | ||||||
|   </.link> |   </.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:") %> | ||||||
|       <%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %> |       <%= if @pack.count == 0, do: gettext("Empty"), else: @pack.count %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span |     <span :if={@original_count && @original_count != @pack.count} class="rounded-lg title text-lg"> | ||||||
|       :if={@original_count && @original_count != @ammo_group.count} |  | ||||||
|       class="rounded-lg title text-lg" |  | ||||||
|     > |  | ||||||
|       <%= gettext("Original Count:") %> |       <%= gettext("Original Count:") %> | ||||||
|       <%= @original_count %> |       <%= @original_count %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span :if={@ammo_group.notes} class="rounded-lg title text-lg"> |     <span :if={@pack.notes} class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Notes:") %> |       <%= gettext("Notes:") %> | ||||||
|       <%= @ammo_group.notes %> |       <%= @pack.notes %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span :if={@ammo_group.purchased_on} class="rounded-lg title text-lg"> |     <span :if={@pack.purchased_on} class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Purchased on:") %> |       <%= gettext("Purchased on:") %> | ||||||
|       <.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} /> |       <.date id={"#{@pack.id}-purchased-on"} date={@pack.purchased_on} /> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span :if={@last_used_date} class="rounded-lg title text-lg"> |     <span :if={@last_used_date} class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Last used on:") %> |       <%= gettext("Last used on:") %> | ||||||
|       <.date id={"#{@ammo_group.id}-last-used-on"} date={@last_used_date} /> |       <.date id={"#{@pack.id}-last-used-on"} date={@last_used_date} /> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span :if={@ammo_group.price_paid} class="rounded-lg title text-lg"> |     <span :if={@pack.price_paid} class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Price paid:") %> |       <%= gettext("Price paid:") %> | ||||||
|       <%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %> |       <%= gettext("$%{amount}", amount: display_currency(@pack.price_paid)) %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span :if={@cpr} class="rounded-lg title text-lg"> |     <span :if={@cpr} class="rounded-lg title text-lg"> | ||||||
| @@ -50,10 +47,15 @@ | |||||||
|       <%= gettext("$%{amount}", amount: display_currency(@cpr)) %> |       <%= gettext("$%{amount}", amount: display_currency(@cpr)) %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|  |     <span :if={@pack.lot_number} class="rounded-lg title text-lg"> | ||||||
|  |       <%= gettext("Lot number:") %> | ||||||
|  |       <%= @pack.lot_number %> | ||||||
|  |     </span> | ||||||
|  | 
 | ||||||
|     <span :if={@container} class="rounded-lg title text-lg"> |     <span :if={@container} class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Container:") %> |       <%= gettext("Container:") %> | ||||||
| 
 | 
 | ||||||
|       <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link"> |       <.link navigate={~p"/container/#{@container}"} class="link"> | ||||||
|         <%= @container.name %> |         <%= @container.name %> | ||||||
|       </.link> |       </.link> | ||||||
|     </span> |     </span> | ||||||
| @@ -1,12 +1,9 @@ | |||||||
| <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-500"> | <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-500"> | ||||||
|   <div class="flex flex-col sm:flex-row justify-between items-center"> |   <div class="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"> | ||||||
|       <.link |       <.link navigate={~p"/"} class="inline mx-2 my-1 leading-5 text-xl text-white"> | ||||||
|         navigate={Routes.live_path(Endpoint, HomeLive)} |  | ||||||
|         class="inline mx-2 my-1 leading-5 text-xl text-white" |  | ||||||
|       > |  | ||||||
|         <img |         <img | ||||||
|           src={Routes.static_path(Endpoint, "/images/cannery.svg")} |           src={~p"/images/cannery.svg"} | ||||||
|           alt={gettext("Cannery logo")} |           alt={gettext("Cannery logo")} | ||||||
|           class="inline-block h-8 mx-1" |           class="inline-block h-8 mx-1" | ||||||
|         /> |         /> | ||||||
| @@ -27,64 +24,43 @@ | |||||||
|       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"> | ||||||
|           <.link |           <.link navigate={~p"/tags"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.tag_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Tags") %> |             <%= gettext("Tags") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link navigate={~p"/containers"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.container_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Containers") %> |             <%= gettext("Containers") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link navigate={~p"/catalog"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.ammo_type_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Catalog") %> |             <%= gettext("Catalog") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link navigate={~p"/ammo"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.ammo_group_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Ammo") %> |             <%= gettext("Ammo") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link navigate={~p"/range"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.range_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Range") %> |             <%= gettext("Range") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1"> |         <li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1"> | ||||||
|           <.link |           <.link navigate={~p"/invites"} class="text-white hover:underline"> | ||||||
|             navigate={Routes.invite_index_path(Endpoint, :index)} |  | ||||||
|             class="text-white hover:underline" |  | ||||||
|           > |  | ||||||
|             <%= gettext("Invites") %> |             <%= gettext("Invites") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link href={~p"/users/settings"} class="text-white hover:underline truncate"> | ||||||
|             href={Routes.user_settings_path(Endpoint, :edit)} |  | ||||||
|             class="text-white hover:underline truncate" |  | ||||||
|           > |  | ||||||
|             <%= @current_user.email %> |             <%= @current_user.email %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link | ||||||
|             href={Routes.user_session_path(Endpoint, :delete)} |             href={~p"/users/log_out"} | ||||||
|             method="delete" |             method="delete" | ||||||
|             data-confirm={dgettext("prompts", "Are you sure you want to log out?")} |             data-confirm={dgettext("prompts", "Are you sure you want to log out?")} | ||||||
|             aria-label={gettext("Log out")} |             aria-label={gettext("Log out")} | ||||||
| @@ -94,13 +70,13 @@ | |||||||
|         </li> |         </li> | ||||||
|         <li |         <li | ||||||
|           :if={ |           :if={ | ||||||
|             @current_user |> Accounts.is_already_admin?() and |             @current_user |> Accounts.already_admin?() and | ||||||
|               function_exported?(Routes, :live_dashboard_path, 2) |               function_exported?(Routes, :live_dashboard_path, 2) | ||||||
|           } |           } | ||||||
|           class="mx-2 my-1" |           class="mx-2 my-1" | ||||||
|         > |         > | ||||||
|           <.link |           <.link | ||||||
|             navigate={Routes.live_dashboard_path(Endpoint, :home)} |             navigate={~p"/dashboard"} | ||||||
|             class="text-white hover:underline" |             class="text-white hover:underline" | ||||||
|             aria-label={gettext("Live Dashboard")} |             aria-label={gettext("Live Dashboard")} | ||||||
|           > |           > | ||||||
| @@ -109,18 +85,12 @@ | |||||||
|         </li> |         </li> | ||||||
|       <% else %> |       <% else %> | ||||||
|         <li :if={Accounts.allow_registration?()} class="mx-2 my-1"> |         <li :if={Accounts.allow_registration?()} class="mx-2 my-1"> | ||||||
|           <.link |           <.link href={~p"/users/register"} class="text-white hover:underline truncate"> | ||||||
|             href={Routes.user_registration_path(Endpoint, :new)} |  | ||||||
|             class="text-white hover:underline truncate" |  | ||||||
|           > |  | ||||||
|             <%= dgettext("actions", "Register") %> |             <%= dgettext("actions", "Register") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|         <li class="mx-2 my-1"> |         <li class="mx-2 my-1"> | ||||||
|           <.link |           <.link href={~p"/users/log_in"} class="text-white hover:underline truncate"> | ||||||
|             href={Routes.user_session_path(Endpoint, :new)} |  | ||||||
|             class="text-white hover:underline truncate" |  | ||||||
|           > |  | ||||||
|             <%= dgettext("actions", "Log in") %> |             <%= dgettext("actions", "Log in") %> | ||||||
|           </.link> |           </.link> | ||||||
|         </li> |         </li> | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								lib/cannery_web/components/email_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/cannery_web/components/email_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | defmodule CanneryWeb.EmailHTML do | ||||||
|  |   @moduledoc """ | ||||||
|  |   Renders email templates | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "email_html/*.html", suffix: "_html" | ||||||
|  |   embed_templates "email_html/*.txt", suffix: "_text" | ||||||
|  | end | ||||||
| @@ -9,4 +9,4 @@ | |||||||
| 
 | 
 | ||||||
| <%= dgettext("emails", | <%= dgettext("emails", | ||||||
|   "If you didn't create an account at %{url}, please ignore this.", |   "If you didn't create an account at %{url}, please ignore this.", | ||||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> |   url: ~p"/") %> | ||||||
| @@ -7,4 +7,4 @@ | |||||||
| 
 | 
 | ||||||
| <%= dgettext("emails", | <%= dgettext("emails", | ||||||
|   "If you didn't request this change from %{url}, please ignore this.", |   "If you didn't request this change from %{url}, please ignore this.", | ||||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> |   url: ~p"/") %> | ||||||
| @@ -7,4 +7,4 @@ | |||||||
| 
 | 
 | ||||||
| <%= dgettext("emails", | <%= dgettext("emails", | ||||||
|   "If you didn't request this change from %{url}, please ignore this.", |   "If you didn't request this change from %{url}, please ignore this.", | ||||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> |   url: ~p"/") %> | ||||||
							
								
								
									
										17
									
								
								lib/cannery_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/cannery_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | defmodule CanneryWeb.Layouts do | ||||||
|  |   @moduledoc """ | ||||||
|  |   The root layouts for the entire application | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "layouts/*" | ||||||
|  |  | ||||||
|  |   def get_title(%{assigns: %{title: title}}) when title not in [nil, ""] do | ||||||
|  |     gettext("Cannery | %{title}", title: title) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def get_title(_conn) do | ||||||
|  |     gettext("Cannery") | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -9,7 +9,7 @@ | |||||||
| 
 | 
 | ||||||
|     <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;" /> |     <hr 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={~p"/"}> | ||||||
|       <%= dgettext( |       <%= dgettext( | ||||||
|         "emails", |         "emails", | ||||||
|         "This email was sent from Cannery, the self-hosted firearm tracker website." |         "This email was sent from Cannery, the self-hosted firearm tracker website." | ||||||
| @@ -8,4 +8,4 @@ | |||||||
| 
 | 
 | ||||||
| <%= dgettext("emails", | <%= dgettext("emails", | ||||||
|   "This email was sent from Cannery at %{url}, the self-hosted firearm tracker website.", |   "This email was sent from Cannery at %{url}, the self-hosted firearm tracker website.", | ||||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> |   url: ~p"/") %> | ||||||
							
								
								
									
										1
									
								
								lib/cannery_web/components/layouts/empty.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/cannery_web/components/layouts/empty.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <%= @inner_block %> | ||||||
| @@ -5,21 +5,12 @@ | |||||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> |     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <%= csrf_meta_tag() %> |     <%= csrf_meta_tag() %> | ||||||
|     <link |     <link rel="shortcut icon" type="image/jpg" href={~p"/images/cannery.svg"} /> | ||||||
|       rel="shortcut icon" |  | ||||||
|       type="image/jpg" |  | ||||||
|       href={Routes.static_path(@conn, "/images/cannery.svg")} |  | ||||||
|     /> |  | ||||||
|     <.live_title suffix={" | #{gettext("Cannery")}"}> |     <.live_title suffix={" | #{gettext("Cannery")}"}> | ||||||
|       <%= assigns[:page_title] || gettext("Cannery") %> |       <%= assigns[:page_title] || gettext("Cannery") %> | ||||||
|     </.live_title> |     </.live_title> | ||||||
|     <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} /> |     <link phx-track-static rel="stylesheet" href={~p"/css/app.css"} /> | ||||||
|     <script |     <script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}> | ||||||
|       defer |  | ||||||
|       phx-track-static |  | ||||||
|       type="text/javascript" |  | ||||||
|       src={Routes.static_path(@conn, "/js/app.js")} |  | ||||||
|     > |  | ||||||
|     </script> |     </script> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
| @@ -1,11 +1,10 @@ | |||||||
| defmodule CanneryWeb.Components.MoveAmmoGroupComponent do | defmodule CanneryWeb.Components.MovePackComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Livecomponent that can move an ammo group to another container |   Livecomponent that can move a pack to another container | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Containers, Containers.Container} |   alias Cannery.{Accounts.User, Ammo, Ammo.Pack, Containers, Containers.Container} | ||||||
|   alias CanneryWeb.Endpoint |  | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|   alias Phoenix.LiveView.Socket |   alias Phoenix.LiveView.Socket | ||||||
| 
 | 
 | ||||||
| @@ -13,17 +12,16 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do | |||||||
|   @spec update( |   @spec update( | ||||||
|           %{ |           %{ | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             required(:ammo_group) => AmmoGroup.t(), |             required(:pack) => Pack.t(), | ||||||
|             optional(any()) => any() |             optional(any()) => any() | ||||||
|           }, |           }, | ||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update( |   def update( | ||||||
|         %{ammo_group: %{container_id: container_id} = ammo_group, current_user: current_user} = |         %{pack: %{container_id: container_id} = pack, current_user: current_user} = assigns, | ||||||
|           assigns, |  | ||||||
|         socket |         socket | ||||||
|       ) do |       ) do | ||||||
|     changeset = ammo_group |> AmmoGroup.update_changeset(%{}, current_user) |     changeset = pack |> Pack.update_changeset(%{}, current_user) | ||||||
| 
 | 
 | ||||||
|     containers = |     containers = | ||||||
|       Containers.list_containers(current_user) |       Containers.list_containers(current_user) | ||||||
| @@ -41,16 +39,15 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do | |||||||
|   def handle_event( |   def handle_event( | ||||||
|         "move", |         "move", | ||||||
|         %{"container_id" => container_id}, |         %{"container_id" => container_id}, | ||||||
|         %{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} = |         %{assigns: %{pack: pack, current_user: current_user, return_to: return_to}} = socket | ||||||
|           socket |  | ||||||
|       ) do |       ) do | ||||||
|     %{name: container_name} = Containers.get_container!(container_id, current_user) |     %{name: container_name} = Containers.get_container!(container_id, current_user) | ||||||
| 
 | 
 | ||||||
|     socket = |     socket = | ||||||
|       ammo_group |       pack | ||||||
|       |> Ammo.update_ammo_group(%{"container_id" => container_id}, current_user) |       |> Ammo.update_pack(%{"container_id" => container_id}, current_user) | ||||||
|       |> case do |       |> case do | ||||||
|         {:ok, _ammo_group} -> |         {:ok, _pack} -> | ||||||
|           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_navigate(to: return_to) |           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) | ||||||
| 
 | 
 | ||||||
| @@ -86,13 +83,13 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do | |||||||
|           <%= display_emoji("😔") %> |           <%= display_emoji("😔") %> | ||||||
|         </h2> |         </h2> | ||||||
| 
 | 
 | ||||||
|         <.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary"> |         <.link navigate={~p"/containers/new"} class="btn btn-primary"> | ||||||
|           <%= dgettext("actions", "Add another container!") %> |           <%= dgettext("actions", "Add another container!") %> | ||||||
|         </.link> |         </.link> | ||||||
|       <% else %> |       <% else %> | ||||||
|         <.live_component |         <.live_component | ||||||
|           module={CanneryWeb.Components.TableComponent} |           module={CanneryWeb.Components.TableComponent} | ||||||
|           id="move_ammo_group_table" |           id="move-pack-table" | ||||||
|           columns={@columns} |           columns={@columns} | ||||||
|           rows={@rows} |           rows={@rows} | ||||||
|         /> |         /> | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| defmodule CanneryWeb.Components.AmmoGroupTableComponent do | defmodule CanneryWeb.Components.PackTableComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   A component that displays a list of ammo groups |   A component that displays a list of packs | ||||||
|   """ |   """ | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, Ammo.AmmoGroup, ComparableDate} |   alias Cannery.{Accounts.User, Ammo.Pack, ComparableDate} | ||||||
|   alias Cannery.{ActivityLog, Ammo, Containers} |   alias Cannery.{ActivityLog, Ammo, Containers} | ||||||
|   alias CanneryWeb.Components.TableComponent |   alias CanneryWeb.Components.TableComponent | ||||||
|   alias Ecto.UUID |   alias Ecto.UUID | ||||||
| @@ -14,9 +14,9 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|           %{ |           %{ | ||||||
|             required(:id) => UUID.t(), |             required(:id) => UUID.t(), | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             required(:ammo_groups) => [AmmoGroup.t()], |             required(:packs) => [Pack.t()], | ||||||
|             required(:show_used) => boolean(), |             required(:show_used) => boolean(), | ||||||
|             optional(:ammo_type) => Rendered.t(), |             optional(:type) => Rendered.t(), | ||||||
|             optional(:range) => Rendered.t(), |             optional(:range) => Rendered.t(), | ||||||
|             optional(:container) => Rendered.t(), |             optional(:container) => Rendered.t(), | ||||||
|             optional(:actions) => Rendered.t(), |             optional(:actions) => Rendered.t(), | ||||||
| @@ -25,28 +25,27 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update( |   def update( | ||||||
|         %{id: _id, ammo_groups: _ammo_group, current_user: _current_user, show_used: _show_used} = |         %{id: _id, packs: _pack, current_user: _current_user, show_used: _show_used} = assigns, | ||||||
|           assigns, |  | ||||||
|         socket |         socket | ||||||
|       ) do |       ) do | ||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(assigns) |       |> assign(assigns) | ||||||
|       |> assign_new(:ammo_type, fn -> [] end) |       |> assign_new(:type, fn -> [] end) | ||||||
|       |> assign_new(:range, fn -> [] end) |       |> assign_new(:range, fn -> [] end) | ||||||
|       |> assign_new(:container, fn -> [] end) |       |> assign_new(:container, fn -> [] end) | ||||||
|       |> assign_new(:actions, fn -> [] end) |       |> assign_new(:actions, fn -> [] end) | ||||||
|       |> display_ammo_groups() |       |> display_packs() | ||||||
| 
 | 
 | ||||||
|     {:ok, socket} |     {:ok, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp display_ammo_groups( |   defp display_packs( | ||||||
|          %{ |          %{ | ||||||
|            assigns: %{ |            assigns: %{ | ||||||
|              ammo_groups: ammo_groups, |              packs: packs, | ||||||
|              current_user: current_user, |              current_user: current_user, | ||||||
|              ammo_type: ammo_type, |              type: type, | ||||||
|              range: range, |              range: range, | ||||||
|              container: container, |              container: container, | ||||||
|              actions: actions, |              actions: actions, | ||||||
| @@ -54,6 +53,9 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|            } |            } | ||||||
|          } = socket |          } = socket | ||||||
|        ) do |        ) do | ||||||
|  |     lot_number_used = packs |> Enum.any?(fn %{lot_number: lot_number} -> !!lot_number end) | ||||||
|  |     price_paid_used = packs |> Enum.any?(fn %{price_paid: price_paid} -> !!price_paid end) | ||||||
|  | 
 | ||||||
|     columns = |     columns = | ||||||
|       [] |       [] | ||||||
|       |> TableComponent.maybe_compose_columns( |       |> TableComponent.maybe_compose_columns( | ||||||
| @@ -78,8 +80,18 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|         %{label: gettext("Range"), key: :range}, |         %{label: gettext("Range"), key: :range}, | ||||||
|         range != [] |         range != [] | ||||||
|       ) |       ) | ||||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("CPR"), key: :cpr}) |       |> TableComponent.maybe_compose_columns( | ||||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("Price paid"), key: :price_paid}) |         %{label: gettext("Lot number"), key: :lot_number}, | ||||||
|  |         lot_number_used | ||||||
|  |       ) | ||||||
|  |       |> TableComponent.maybe_compose_columns( | ||||||
|  |         %{label: gettext("CPR"), key: :cpr}, | ||||||
|  |         price_paid_used | ||||||
|  |       ) | ||||||
|  |       |> TableComponent.maybe_compose_columns( | ||||||
|  |         %{label: gettext("Price paid"), key: :price_paid}, | ||||||
|  |         price_paid_used | ||||||
|  |       ) | ||||||
|       |> TableComponent.maybe_compose_columns( |       |> TableComponent.maybe_compose_columns( | ||||||
|         %{label: gettext("% left"), key: :remaining}, |         %{label: gettext("% left"), key: :remaining}, | ||||||
|         show_used |         show_used | ||||||
| @@ -93,33 +105,33 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|         key: :count |         key: :count | ||||||
|       }) |       }) | ||||||
|       |> TableComponent.maybe_compose_columns( |       |> TableComponent.maybe_compose_columns( | ||||||
|         %{label: gettext("Ammo type"), key: :ammo_type}, |         %{label: gettext("Type"), key: :type}, | ||||||
|         ammo_type != [] |         type != [] | ||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
|     containers = |     containers = | ||||||
|       ammo_groups |       packs | ||||||
|       |> Enum.map(fn %{container_id: container_id} -> container_id end) |       |> Enum.map(fn %{container_id: container_id} -> container_id end) | ||||||
|       |> Containers.get_containers(current_user) |       |> Containers.get_containers(current_user) | ||||||
| 
 | 
 | ||||||
|     extra_data = %{ |     extra_data = %{ | ||||||
|       current_user: current_user, |       current_user: current_user, | ||||||
|       ammo_type: ammo_type, |       type: type, | ||||||
|       columns: columns, |       columns: columns, | ||||||
|       container: container, |       container: container, | ||||||
|       containers: containers, |       containers: containers, | ||||||
|       original_counts: Ammo.get_original_counts(ammo_groups, current_user), |       original_counts: Ammo.get_original_counts(packs, current_user), | ||||||
|       cprs: Ammo.get_cprs(ammo_groups, current_user), |       cprs: Ammo.get_cprs(packs, current_user), | ||||||
|       last_used_dates: ActivityLog.get_last_used_dates(ammo_groups, current_user), |       last_used_dates: ActivityLog.get_last_used_dates(packs, current_user), | ||||||
|       percentages_remaining: Ammo.get_percentages_remaining(ammo_groups, current_user), |       percentages_remaining: Ammo.get_percentages_remaining(packs, current_user), | ||||||
|       actions: actions, |       actions: actions, | ||||||
|       range: range |       range: range | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     rows = |     rows = | ||||||
|       ammo_groups |       packs | ||||||
|       |> Enum.map(fn ammo_group -> |       |> Enum.map(fn pack -> | ||||||
|         ammo_group |> get_row_data_for_ammo_group(extra_data) |         pack |> get_row_data_for_pack(extra_data) | ||||||
|       end) |       end) | ||||||
| 
 | 
 | ||||||
|     socket |> assign(columns: columns, rows: rows) |     socket |> assign(columns: columns, rows: rows) | ||||||
| @@ -129,31 +141,36 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|   def render(assigns) do |   def render(assigns) do | ||||||
|     ~H""" |     ~H""" | ||||||
|     <div id={@id} class="w-full"> |     <div id={@id} class="w-full"> | ||||||
|       <.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} /> |       <.live_component | ||||||
|  |         module={TableComponent} | ||||||
|  |         id={"pack-table-#{@id}"} | ||||||
|  |         columns={@columns} | ||||||
|  |         rows={@rows} | ||||||
|  |       /> | ||||||
|     </div> |     </div> | ||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec get_row_data_for_ammo_group(AmmoGroup.t(), additional_data :: map()) :: map() |   @spec get_row_data_for_pack(Pack.t(), additional_data :: map()) :: map() | ||||||
|   defp get_row_data_for_ammo_group(ammo_group, %{columns: columns} = additional_data) do |   defp get_row_data_for_pack(pack, %{columns: columns} = additional_data) do | ||||||
|     columns |     columns | ||||||
|     |> Map.new(fn %{key: key} -> |     |> Map.new(fn %{key: key} -> | ||||||
|       {key, get_value_for_key(key, ammo_group, additional_data)} |       {key, get_value_for_key(key, pack, additional_data)} | ||||||
|     end) |     end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec get_value_for_key(atom(), AmmoGroup.t(), additional_data :: map()) :: |   @spec get_value_for_key(atom(), Pack.t(), additional_data :: map()) :: | ||||||
|           any() | {any(), Rendered.t()} |           any() | {any(), Rendered.t()} | ||||||
|   defp get_value_for_key( |   defp get_value_for_key( | ||||||
|          :ammo_type, |          :type, | ||||||
|          %{ammo_type: %{name: ammo_type_name} = ammo_type}, |          %{type: %{name: type_name} = type}, | ||||||
|          %{ammo_type: ammo_type_block} |          %{type: type_block} | ||||||
|        ) do |        ) do | ||||||
|     assigns = %{ammo_type: ammo_type, ammo_type_block: ammo_type_block} |     assigns = %{type: type, type_block: type_block} | ||||||
| 
 | 
 | ||||||
|     {ammo_type_name, |     {type_name, | ||||||
|      ~H""" |      ~H""" | ||||||
|      <%= render_slot(@ammo_type_block, @ammo_type) %> |      <%= render_slot(@type_block, @type) %> | ||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -170,9 +187,9 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:used_up_on, %{id: ammo_group_id}, %{last_used_dates: last_used_dates}) do |   defp get_value_for_key(:used_up_on, %{id: pack_id}, %{last_used_dates: last_used_dates}) do | ||||||
|     last_used_date = last_used_dates |> Map.get(ammo_group_id) |     last_used_date = last_used_dates |> Map.get(pack_id) | ||||||
|     assigns = %{id: ammo_group_id, last_used_date: last_used_date} |     assigns = %{id: pack_id, last_used_date: last_used_date} | ||||||
| 
 | 
 | ||||||
|     {last_used_date, |     {last_used_date, | ||||||
|      ~H""" |      ~H""" | ||||||
| @@ -184,29 +201,29 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:range, %{staged: staged} = ammo_group, %{range: range}) do |   defp get_value_for_key(:range, %{staged: staged} = pack, %{range: range}) do | ||||||
|     assigns = %{range: range, ammo_group: ammo_group} |     assigns = %{range: range, pack: pack} | ||||||
| 
 | 
 | ||||||
|     {staged, |     {staged, | ||||||
|      ~H""" |      ~H""" | ||||||
|      <%= render_slot(@range, @ammo_group) %> |      <%= render_slot(@range, @pack) %> | ||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key( |   defp get_value_for_key( | ||||||
|          :remaining, |          :remaining, | ||||||
|          %{id: ammo_group_id}, |          %{id: pack_id}, | ||||||
|          %{percentages_remaining: percentages_remaining} |          %{percentages_remaining: percentages_remaining} | ||||||
|        ) do |        ) do | ||||||
|     percentage = Map.fetch!(percentages_remaining, ammo_group_id) |     percentage = Map.fetch!(percentages_remaining, pack_id) | ||||||
|     {percentage, gettext("%{percentage}%", percentage: percentage)} |     {percentage, gettext("%{percentage}%", percentage: percentage)} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:actions, ammo_group, %{actions: actions}) do |   defp get_value_for_key(:actions, pack, %{actions: actions}) do | ||||||
|     assigns = %{actions: actions, ammo_group: ammo_group} |     assigns = %{actions: actions, pack: pack} | ||||||
| 
 | 
 | ||||||
|     ~H""" |     ~H""" | ||||||
|     <%= render_slot(@actions, @ammo_group) %> |     <%= render_slot(@actions, @pack) %> | ||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -214,7 +231,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
| 
 | 
 | ||||||
|   defp get_value_for_key( |   defp get_value_for_key( | ||||||
|          :container, |          :container, | ||||||
|          %{container_id: container_id} = ammo_group, |          %{container_id: container_id} = pack, | ||||||
|          %{container: container_block, containers: containers} |          %{container: container_block, containers: containers} | ||||||
|        ) do |        ) do | ||||||
|     container = %{name: container_name} = Map.fetch!(containers, container_id) |     container = %{name: container_name} = Map.fetch!(containers, container_id) | ||||||
| @@ -222,35 +239,35 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do | |||||||
|     assigns = %{ |     assigns = %{ | ||||||
|       container: container, |       container: container, | ||||||
|       container_block: container_block, |       container_block: container_block, | ||||||
|       ammo_group: ammo_group |       pack: pack | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     {container_name, |     {container_name, | ||||||
|      ~H""" |      ~H""" | ||||||
|      <%= render_slot(@container_block, {@ammo_group, @container}) %> |      <%= render_slot(@container_block, {@pack, @container}) %> | ||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key( |   defp get_value_for_key( | ||||||
|          :original_count, |          :original_count, | ||||||
|          %{id: ammo_group_id}, |          %{id: pack_id}, | ||||||
|          %{original_counts: original_counts} |          %{original_counts: original_counts} | ||||||
|        ) do |        ) do | ||||||
|     Map.fetch!(original_counts, ammo_group_id) |     Map.fetch!(original_counts, pack_id) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data), |   defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data), | ||||||
|     do: {0, gettext("No cost information")} |     do: {0, gettext("No cost information")} | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:cpr, %{id: ammo_group_id}, %{cprs: cprs}) do |   defp get_value_for_key(:cpr, %{id: pack_id}, %{cprs: cprs}) do | ||||||
|     amount = Map.fetch!(cprs, ammo_group_id) |     amount = Map.fetch!(cprs, pack_id) | ||||||
|     {amount, gettext("$%{amount}", amount: display_currency(amount))} |     {amount, gettext("$%{amount}", amount: display_currency(amount))} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(:count, %{count: count}, _additional_data), |   defp get_value_for_key(:count, %{count: count}, _additional_data), | ||||||
|     do: if(count == 0, do: {0, gettext("Empty")}, else: count) |     do: if(count == 0, do: {0, gettext("Empty")}, else: count) | ||||||
| 
 | 
 | ||||||
|   defp get_value_for_key(key, ammo_group, _additional_data), do: ammo_group |> Map.get(key) |   defp get_value_for_key(key, pack, _additional_data), do: pack |> Map.get(key) | ||||||
| 
 | 
 | ||||||
|   @spec display_currency(float()) :: String.t() |   @spec display_currency(float()) :: String.t() | ||||||
|   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) |   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| defmodule CanneryWeb.Components.ShotGroupTableComponent do | defmodule CanneryWeb.Components.ShotRecordTableComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   A component that displays a list of shot groups |   A component that displays a list of shot records | ||||||
|   """ |   """ | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo, ComparableDate} |   alias Cannery.{Accounts.User, ActivityLog.ShotRecord, Ammo, ComparableDate} | ||||||
|   alias Ecto.UUID |   alias Ecto.UUID | ||||||
|   alias Phoenix.LiveView.{Rendered, Socket} |   alias Phoenix.LiveView.{Rendered, Socket} | ||||||
| 
 | 
 | ||||||
| @@ -12,26 +12,29 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do | |||||||
|           %{ |           %{ | ||||||
|             required(:id) => UUID.t(), |             required(:id) => UUID.t(), | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             optional(:shot_groups) => [ShotGroup.t()], |             optional(:shot_records) => [ShotRecord.t()], | ||||||
|             optional(:actions) => Rendered.t(), |             optional(:actions) => Rendered.t(), | ||||||
|             optional(any()) => any() |             optional(any()) => any() | ||||||
|           }, |           }, | ||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update(%{id: _id, shot_groups: _shot_groups, current_user: _current_user} = assigns, socket) do |   def update( | ||||||
|  |         %{id: _id, shot_records: _shot_records, current_user: _current_user} = assigns, | ||||||
|  |         socket | ||||||
|  |       ) do | ||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(assigns) |       |> assign(assigns) | ||||||
|       |> assign_new(:actions, fn -> [] end) |       |> assign_new(:actions, fn -> [] end) | ||||||
|       |> display_shot_groups() |       |> display_shot_records() | ||||||
| 
 | 
 | ||||||
|     {:ok, socket} |     {:ok, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp display_shot_groups( |   defp display_shot_records( | ||||||
|          %{ |          %{ | ||||||
|            assigns: %{ |            assigns: %{ | ||||||
|              shot_groups: shot_groups, |              shot_records: shot_records, | ||||||
|              current_user: current_user, |              current_user: current_user, | ||||||
|              actions: actions |              actions: actions | ||||||
|            } |            } | ||||||
| @@ -45,17 +48,17 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do | |||||||
|       %{label: gettext("Actions"), key: :actions, sortable: false} |       %{label: gettext("Actions"), key: :actions, sortable: false} | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     ammo_groups = |     packs = | ||||||
|       shot_groups |       shot_records | ||||||
|       |> Enum.map(fn %{ammo_group_id: ammo_group_id} -> ammo_group_id end) |       |> Enum.map(fn %{pack_id: pack_id} -> pack_id end) | ||||||
|       |> Ammo.get_ammo_groups(current_user) |       |> Ammo.get_packs(current_user) | ||||||
| 
 | 
 | ||||||
|     extra_data = %{current_user: current_user, actions: actions, ammo_groups: ammo_groups} |     extra_data = %{current_user: current_user, actions: actions, packs: packs} | ||||||
| 
 | 
 | ||||||
|     rows = |     rows = | ||||||
|       shot_groups |       shot_records | ||||||
|       |> Enum.map(fn shot_group -> |       |> Enum.map(fn shot_record -> | ||||||
|         shot_group |> get_row_data_for_shot_group(columns, extra_data) |         shot_record |> get_row_data_for_shot_record(columns, extra_data) | ||||||
|       end) |       end) | ||||||
| 
 | 
 | ||||||
|     socket |     socket | ||||||
| @@ -71,7 +74,7 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do | |||||||
|     <div id={@id} class="w-full"> |     <div id={@id} class="w-full"> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.TableComponent} |         module={CanneryWeb.Components.TableComponent} | ||||||
|         id={"table-#{@id}"} |         id={"shot-record-table-#{@id}"} | ||||||
|         columns={@columns} |         columns={@columns} | ||||||
|         rows={@rows} |         rows={@rows} | ||||||
|         initial_key={:date} |         initial_key={:date} | ||||||
| @@ -81,22 +84,22 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do | |||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec get_row_data_for_shot_group(ShotGroup.t(), columns :: [map()], extra_data :: map()) :: |   @spec get_row_data_for_shot_record(ShotRecord.t(), columns :: [map()], extra_data :: map()) :: | ||||||
|           map() |           map() | ||||||
|   defp get_row_data_for_shot_group(shot_group, columns, extra_data) do |   defp get_row_data_for_shot_record(shot_record, columns, extra_data) do | ||||||
|     columns |     columns | ||||||
|     |> Map.new(fn %{key: key} -> |     |> Map.new(fn %{key: key} -> | ||||||
|       {key, get_row_value(key, shot_group, extra_data)} |       {key, get_row_value(key, shot_record, extra_data)} | ||||||
|     end) |     end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_row_value(:name, %{ammo_group_id: ammo_group_id}, %{ammo_groups: ammo_groups}) do |   defp get_row_value(:name, %{pack_id: pack_id}, %{packs: packs}) do | ||||||
|     assigns = %{ammo_group: ammo_group = Map.fetch!(ammo_groups, ammo_group_id)} |     assigns = %{pack: pack = Map.fetch!(packs, pack_id)} | ||||||
| 
 | 
 | ||||||
|     {ammo_group.ammo_type.name, |     {pack.type.name, | ||||||
|      ~H""" |      ~H""" | ||||||
|      <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="link"> |      <.link navigate={~p"/ammo/show/#{@pack}"} class="link"> | ||||||
|        <%= @ammo_group.ammo_type.name %> |        <%= @pack.type.name %> | ||||||
|      </.link> |      </.link> | ||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| @@ -108,13 +111,13 @@ defmodule CanneryWeb.Components.ShotGroupTableComponent do | |||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_row_value(:actions, shot_group, %{actions: actions}) do |   defp get_row_value(:actions, shot_record, %{actions: actions}) do | ||||||
|     assigns = %{actions: actions, shot_group: shot_group} |     assigns = %{actions: actions, shot_record: shot_record} | ||||||
| 
 | 
 | ||||||
|     ~H""" |     ~H""" | ||||||
|     <%= render_slot(@actions, @shot_group) %> |     <%= render_slot(@actions, @shot_record) %> | ||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_row_value(key, shot_group, _extra_data), do: shot_group |> Map.get(key) |   defp get_row_value(key, shot_record, _extra_data), do: shot_record |> Map.get(key) | ||||||
| end | end | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| defmodule CanneryWeb.Components.AmmoTypeTableComponent do | defmodule CanneryWeb.Components.TypeTableComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   A component that displays a list of ammo type |   A component that displays a list of types | ||||||
|   """ |   """ | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoType} |   alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.Type} | ||||||
|   alias CanneryWeb.Components.TableComponent |   alias CanneryWeb.Components.TableComponent | ||||||
|   alias Ecto.UUID |   alias Ecto.UUID | ||||||
|   alias Phoenix.LiveView.{Rendered, Socket} |   alias Phoenix.LiveView.{Rendered, Socket} | ||||||
| @@ -13,33 +13,33 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|           %{ |           %{ | ||||||
|             required(:id) => UUID.t(), |             required(:id) => UUID.t(), | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             optional(:type) => AmmoType.type() | nil, |             optional(:class) => Type.class() | nil, | ||||||
|             optional(:show_used) => boolean(), |             optional(:show_used) => boolean(), | ||||||
|             optional(:ammo_types) => [AmmoType.t()], |             optional(:types) => [Type.t()], | ||||||
|             optional(:actions) => Rendered.t(), |             optional(:actions) => Rendered.t(), | ||||||
|             optional(any()) => any() |             optional(any()) => any() | ||||||
|           }, |           }, | ||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update(%{id: _id, ammo_types: _ammo_types, current_user: _current_user} = assigns, socket) do |   def update(%{id: _id, types: _types, current_user: _current_user} = assigns, socket) do | ||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(assigns) |       |> assign(assigns) | ||||||
|       |> assign_new(:show_used, fn -> false end) |       |> assign_new(:show_used, fn -> false end) | ||||||
|       |> assign_new(:type, fn -> :all end) |       |> assign_new(:class, fn -> :all end) | ||||||
|       |> assign_new(:actions, fn -> [] end) |       |> assign_new(:actions, fn -> [] end) | ||||||
|       |> display_ammo_types() |       |> display_types() | ||||||
| 
 | 
 | ||||||
|     {:ok, socket} |     {:ok, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp display_ammo_types( |   defp display_types( | ||||||
|          %{ |          %{ | ||||||
|            assigns: %{ |            assigns: %{ | ||||||
|              ammo_types: ammo_types, |              types: types, | ||||||
|              current_user: current_user, |              current_user: current_user, | ||||||
|              show_used: show_used, |              show_used: show_used, | ||||||
|              type: type, |              class: class, | ||||||
|              actions: actions |              actions: actions | ||||||
|            } |            } | ||||||
|          } = socket |          } = socket | ||||||
| @@ -48,18 +48,17 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|       [ |       [ | ||||||
|         %{label: gettext("Cartridge"), key: :cartridge, type: :string}, |         %{label: gettext("Cartridge"), key: :cartridge, type: :string}, | ||||||
|         %{ |         %{ | ||||||
|           label: if(type == :shotgun, do: gettext("Gauge"), else: gettext("Caliber")), |           label: if(class == :shotgun, do: gettext("Gauge"), else: gettext("Caliber")), | ||||||
|           key: :caliber, |           key: :caliber, | ||||||
|           type: :string |           type: :string | ||||||
|         }, |         }, | ||||||
|         %{label: gettext("Unfired shell length"), key: :unfired_length, type: :string}, |         %{label: gettext("Unfired shell length"), key: :unfired_length, type: :string}, | ||||||
|         %{label: gettext("Brass height"), key: :brass_height, type: :string}, |         %{label: gettext("Brass height"), key: :brass_height, type: :string}, | ||||||
|         %{label: gettext("Chamber size"), key: :chamber_size, type: :string}, |         %{label: gettext("Chamber size"), key: :chamber_size, type: :string}, | ||||||
|         %{label: gettext("Chamber size"), key: :chamber_size, type: :string}, |  | ||||||
|         %{label: gettext("Grains"), key: :grains, type: :string}, |         %{label: gettext("Grains"), key: :grains, type: :string}, | ||||||
|         %{label: gettext("Bullet type"), key: :bullet_type, type: :string}, |         %{label: gettext("Bullet type"), key: :bullet_type, type: :string}, | ||||||
|         %{ |         %{ | ||||||
|           label: if(type == :shotgun, do: gettext("Slug core"), else: gettext("Bullet core")), |           label: if(class == :shotgun, do: gettext("Slug core"), else: gettext("Bullet core")), | ||||||
|           key: :bullet_core, |           key: :bullet_core, | ||||||
|           type: :string |           type: :string | ||||||
|         }, |         }, | ||||||
| @@ -92,8 +91,8 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|         # remove columns if all values match defaults |         # remove columns if all values match defaults | ||||||
|         default_value = if type == :atom, do: false, else: nil |         default_value = if type == :atom, do: false, else: nil | ||||||
| 
 | 
 | ||||||
|         ammo_types |         types | ||||||
|         |> Enum.any?(fn ammo_type -> Map.get(ammo_type, key, default_value) != default_value end) |         |> Enum.any?(fn type -> Map.get(type, key, default_value) != default_value end) | ||||||
|       end) |       end) | ||||||
| 
 | 
 | ||||||
|     columns = |     columns = | ||||||
| @@ -147,22 +146,30 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|       }) |       }) | ||||||
|       |> TableComponent.maybe_compose_columns(filtered_columns) |       |> TableComponent.maybe_compose_columns(filtered_columns) | ||||||
|       |> TableComponent.maybe_compose_columns( |       |> TableComponent.maybe_compose_columns( | ||||||
|         %{label: gettext("Type"), key: :type, type: :atom}, |         %{label: gettext("Class"), key: :class, type: :atom}, | ||||||
|         type in [:all, nil] |         class in [:all, nil] | ||||||
|       ) |       ) | ||||||
|       |> TableComponent.maybe_compose_columns(%{label: gettext("Name"), key: :name, type: :name}) |       |> TableComponent.maybe_compose_columns(%{label: gettext("Name"), key: :name, type: :name}) | ||||||
| 
 | 
 | ||||||
|     round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user) |     round_counts = Ammo.get_grouped_round_count(current_user, types: types, group_by: :type_id) | ||||||
|     packs_count = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user) |     packs_count = Ammo.get_grouped_packs_count(current_user, types: types, group_by: :type_id) | ||||||
|     average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user) |     average_costs = Ammo.get_average_costs(types, current_user) | ||||||
| 
 | 
 | ||||||
|     [used_counts, historical_round_counts, historical_pack_counts, used_pack_counts] = |     [used_counts, historical_round_counts, historical_pack_counts, used_pack_counts] = | ||||||
|       if show_used do |       if show_used do | ||||||
|         [ |         [ | ||||||
|           ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user), |           ActivityLog.get_grouped_used_counts(current_user, types: types, group_by: :type_id), | ||||||
|           ammo_types |> Ammo.get_historical_count_for_ammo_types(current_user), |           Ammo.get_historical_counts(types, current_user), | ||||||
|           ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true), |           Ammo.get_grouped_packs_count(current_user, | ||||||
|           ammo_types |> Ammo.get_used_ammo_groups_count_for_types(current_user) |             types: types, | ||||||
|  |             group_by: :type_id, | ||||||
|  |             show_used: true | ||||||
|  |           ), | ||||||
|  |           Ammo.get_grouped_packs_count(current_user, | ||||||
|  |             types: types, | ||||||
|  |             group_by: :type_id, | ||||||
|  |             show_used: :only_used | ||||||
|  |           ) | ||||||
|         ] |         ] | ||||||
|       else |       else | ||||||
|         [nil, nil, nil, nil] |         [nil, nil, nil, nil] | ||||||
| @@ -181,9 +188,9 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     rows = |     rows = | ||||||
|       ammo_types |       types | ||||||
|       |> Enum.map(fn ammo_type -> |       |> Enum.map(fn type -> | ||||||
|         ammo_type |> get_ammo_type_values(columns, extra_data) |         type |> get_type_values(columns, extra_data) | ||||||
|       end) |       end) | ||||||
| 
 | 
 | ||||||
|     socket |> assign(columns: columns, rows: rows) |     socket |> assign(columns: columns, rows: rows) | ||||||
| @@ -193,97 +200,100 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do | |||||||
|   def render(assigns) do |   def render(assigns) do | ||||||
|     ~H""" |     ~H""" | ||||||
|     <div id={@id} class="w-full"> |     <div id={@id} class="w-full"> | ||||||
|       <.live_component module={TableComponent} id={"table-#{@id}"} columns={@columns} rows={@rows} /> |       <.live_component | ||||||
|  |         module={TableComponent} | ||||||
|  |         id={"type-table-#{@id}"} | ||||||
|  |         columns={@columns} | ||||||
|  |         rows={@rows} | ||||||
|  |       /> | ||||||
|     </div> |     </div> | ||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_values(ammo_type, columns, extra_data) do |   defp get_type_values(type, columns, extra_data) do | ||||||
|     columns |     columns | ||||||
|     |> Map.new(fn %{key: key, type: type} -> |     |> Map.new(fn %{key: key, type: column_type} -> | ||||||
|       {key, get_ammo_type_value(type, key, ammo_type, extra_data)} |       {key, get_type_value(column_type, key, type, extra_data)} | ||||||
|     end) |     end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(:atom, key, ammo_type, _other_data), |   defp get_type_value(:atom, key, type, _other_data), | ||||||
|     do: ammo_type |> Map.get(key) |> humanize() |     do: type |> Map.get(key) |> humanize() | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(:round_count, _key, %{id: ammo_type_id}, %{round_counts: round_counts}), |   defp get_type_value(:round_count, _key, %{id: type_id}, %{round_counts: round_counts}), | ||||||
|     do: Map.get(round_counts, ammo_type_id, 0) |     do: Map.get(round_counts, type_id, 0) | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value( |   defp get_type_value( | ||||||
|          :historical_round_count, |          :historical_round_count, | ||||||
|          _key, |          _key, | ||||||
|          %{id: ammo_type_id}, |          %{id: type_id}, | ||||||
|          %{historical_round_counts: historical_round_counts} |          %{historical_round_counts: historical_round_counts} | ||||||
|        ) do |        ) do | ||||||
|     Map.get(historical_round_counts, ammo_type_id, 0) |     Map.get(historical_round_counts, type_id, 0) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value( |   defp get_type_value( | ||||||
|          :used_round_count, |          :used_round_count, | ||||||
|          _key, |          _key, | ||||||
|          %{id: ammo_type_id}, |          %{id: type_id}, | ||||||
|          %{used_counts: used_counts} |          %{used_counts: used_counts} | ||||||
|        ) do |        ) do | ||||||
|     Map.get(used_counts, ammo_type_id, 0) |     Map.get(used_counts, type_id, 0) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value( |   defp get_type_value( | ||||||
|          :historical_pack_count, |          :historical_pack_count, | ||||||
|          _key, |          _key, | ||||||
|          %{id: ammo_type_id}, |          %{id: type_id}, | ||||||
|          %{historical_pack_counts: historical_pack_counts} |          %{historical_pack_counts: historical_pack_counts} | ||||||
|        ) do |        ) do | ||||||
|     Map.get(historical_pack_counts, ammo_type_id, 0) |     Map.get(historical_pack_counts, type_id, 0) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value( |   defp get_type_value( | ||||||
|          :used_pack_count, |          :used_pack_count, | ||||||
|          _key, |          _key, | ||||||
|          %{id: ammo_type_id}, |          %{id: type_id}, | ||||||
|          %{used_pack_counts: used_pack_counts} |          %{used_pack_counts: used_pack_counts} | ||||||
|        ) do |        ) do | ||||||
|     Map.get(used_pack_counts, ammo_type_id, 0) |     Map.get(used_pack_counts, type_id, 0) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(:ammo_count, _key, %{id: ammo_type_id}, %{packs_count: packs_count}), |   defp get_type_value(:ammo_count, _key, %{id: type_id}, %{packs_count: packs_count}), | ||||||
|     do: Map.get(packs_count, ammo_type_id) |     do: Map.get(packs_count, type_id) | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value( |   defp get_type_value( | ||||||
|          :avg_price_paid, |          :avg_price_paid, | ||||||
|          _key, |          _key, | ||||||
|          %{id: ammo_type_id}, |          %{id: type_id}, | ||||||
|          %{average_costs: average_costs} |          %{average_costs: average_costs} | ||||||
|        ) do |        ) do | ||||||
|     case Map.get(average_costs, ammo_type_id) do |     case Map.get(average_costs, type_id) do | ||||||
|       nil -> {0, gettext("No cost information")} |       nil -> {0, gettext("No cost information")} | ||||||
|       count -> {count, gettext("$%{amount}", amount: display_currency(count))} |       count -> {count, gettext("$%{amount}", amount: display_currency(count))} | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(:name, _key, %{name: ammo_type_name} = ammo_type, _other_data) do |   defp get_type_value(:name, _key, %{name: type_name} = assigns, _other_data) do | ||||||
|     assigns = %{ammo_type: ammo_type} |     {type_name, | ||||||
| 
 |  | ||||||
|     {ammo_type_name, |  | ||||||
|      ~H""" |      ~H""" | ||||||
|      <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_type)} class="link"> |      <.link navigate={~p"/type/#{@id}"} class="link"> | ||||||
|        <%= @ammo_type.name %> |        <%= @name %> | ||||||
|      </.link> |      </.link> | ||||||
|      """} |      """} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(:actions, _key, ammo_type, %{actions: actions}) do |   defp get_type_value(:actions, _key, type, %{actions: actions}) do | ||||||
|     assigns = %{actions: actions, ammo_type: ammo_type} |     assigns = %{actions: actions, type: type} | ||||||
| 
 | 
 | ||||||
|     ~H""" |     ~H""" | ||||||
|     <%= render_slot(@actions, @ammo_type) %> |     <%= render_slot(@actions, @type) %> | ||||||
|     """ |     """ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(nil, _key, _ammo_type, _other_data), do: nil |   defp get_type_value(nil, _key, _type, _other_data), do: nil | ||||||
| 
 | 
 | ||||||
|   defp get_ammo_type_value(_other, key, ammo_type, _other_data), do: ammo_type |> Map.get(key) |   defp get_type_value(_other, key, type, _other_data), do: type |> Map.get(key) | ||||||
| 
 | 
 | ||||||
|   @spec display_currency(float()) :: String.t() |   @spec display_currency(float()) :: String.t() | ||||||
|   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) |   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) | ||||||
| @@ -6,7 +6,8 @@ defmodule CanneryWeb.EmailController do | |||||||
|   use CanneryWeb, :controller |   use CanneryWeb, :controller | ||||||
|   alias Cannery.Accounts.User |   alias Cannery.Accounts.User | ||||||
|  |  | ||||||
|   plug :put_layout, {CanneryWeb.LayoutView, :email} |   plug :put_root_layout, html: {CanneryWeb.Layouts, :email_html} | ||||||
|  |   plug :put_layout, false | ||||||
|  |  | ||||||
|   @sample_assigns %{ |   @sample_assigns %{ | ||||||
|     email: %{subject: "Example subject"}, |     email: %{subject: "Example subject"}, | ||||||
| @@ -18,6 +19,6 @@ defmodule CanneryWeb.EmailController do | |||||||
|   Debug route used to preview emails |   Debug route used to preview emails | ||||||
|   """ |   """ | ||||||
|   def preview(conn, %{"id" => template}) do |   def preview(conn, %{"id" => template}) do | ||||||
|     render(conn, "#{template |> to_string()}.html", @sample_assigns) |     render(conn, String.to_existing_atom(template), @sample_assigns) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								lib/cannery_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/cannery_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | defmodule CanneryWeb.ErrorHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "error_html/*" | ||||||
|  |  | ||||||
|  |   def render(template, _assigns) do | ||||||
|  |     error_string = | ||||||
|  |       case template do | ||||||
|  |         "404.html" -> dgettext("errors", "Not found") | ||||||
|  |         "401.html" -> dgettext("errors", "Unauthorized") | ||||||
|  |         _other_path -> dgettext("errors", "Internal server error") | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |     error(%{error_string: error_string}) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -24,10 +24,7 @@ | |||||||
| 
 | 
 | ||||||
|         <hr class="w-full hr" /> |         <hr class="w-full hr" /> | ||||||
| 
 | 
 | ||||||
|         <.link |         <.link href={~p"/"} class="link title text-primary-600 text-lg"> | ||||||
|           href={Routes.live_path(Endpoint, HomeLive)} |  | ||||||
|           class="link title text-primary-600 text-lg" |  | ||||||
|         > |  | ||||||
|           <%= dgettext("errors", "Go back home") %> |           <%= dgettext("errors", "Go back home") %> | ||||||
|         </.link> |         </.link> | ||||||
|       </div> |       </div> | ||||||
							
								
								
									
										14
									
								
								lib/cannery_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/cannery_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | defmodule CanneryWeb.ErrorJSON do | ||||||
|  |   import CanneryWeb.Gettext | ||||||
|  |  | ||||||
|  |   def render(template, _assigns) do | ||||||
|  |     error_string = | ||||||
|  |       case template do | ||||||
|  |         "404.json" -> dgettext("errors", "Not found") | ||||||
|  |         "401.json" -> dgettext("errors", "Unauthorized") | ||||||
|  |         _other_path -> dgettext("errors", "Internal server error") | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |     %{errors: %{detail: error_string}} | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -3,73 +3,80 @@ defmodule CanneryWeb.ExportController do | |||||||
|   alias Cannery.{ActivityLog, Ammo, Containers} |   alias Cannery.{ActivityLog, Ammo, Containers} | ||||||
|  |  | ||||||
|   def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do |   def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do | ||||||
|     ammo_types = Ammo.list_ammo_types(current_user, :all) |     types = Ammo.list_types(current_user) | ||||||
|     used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user) |  | ||||||
|     round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user) |  | ||||||
|     ammo_group_counts = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user) |  | ||||||
|  |  | ||||||
|     total_ammo_group_counts = |     used_counts = | ||||||
|       ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true) |       ActivityLog.get_grouped_used_counts(current_user, types: types, group_by: :type_id) | ||||||
|  |  | ||||||
|     average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user) |     round_counts = Ammo.get_grouped_round_count(current_user, types: types, group_by: :type_id) | ||||||
|  |     pack_counts = Ammo.get_grouped_packs_count(current_user, types: types, group_by: :type_id) | ||||||
|  |  | ||||||
|     ammo_types = |     total_pack_counts = | ||||||
|       ammo_types |       Ammo.get_grouped_packs_count(current_user, | ||||||
|       |> Enum.map(fn %{id: ammo_type_id} = ammo_type -> |         types: types, | ||||||
|         ammo_type |         group_by: :type_id, | ||||||
|  |         show_used: true | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |     average_costs = Ammo.get_average_costs(types, current_user) | ||||||
|  |  | ||||||
|  |     types = | ||||||
|  |       types | ||||||
|  |       |> Enum.map(fn %{id: type_id} = type -> | ||||||
|  |         type | ||||||
|         |> Jason.encode!() |         |> Jason.encode!() | ||||||
|         |> Jason.decode!() |         |> Jason.decode!() | ||||||
|         |> Map.merge(%{ |         |> Map.merge(%{ | ||||||
|           "average_cost" => Map.get(average_costs, ammo_type_id), |           "average_cost" => Map.get(average_costs, type_id), | ||||||
|           "round_count" => Map.get(round_counts, ammo_type_id, 0), |           "round_count" => Map.get(round_counts, type_id, 0), | ||||||
|           "used_count" => Map.get(used_counts, ammo_type_id, 0), |           "used_count" => Map.get(used_counts, type_id, 0), | ||||||
|           "ammo_group_count" => Map.get(ammo_group_counts, ammo_type_id, 0), |           "pack_count" => Map.get(pack_counts, type_id, 0), | ||||||
|           "total_ammo_group_count" => Map.get(total_ammo_group_counts, ammo_type_id, 0) |           "total_pack_count" => Map.get(total_pack_counts, type_id, 0) | ||||||
|         }) |         }) | ||||||
|       end) |       end) | ||||||
|  |  | ||||||
|     ammo_groups = Ammo.list_ammo_groups(nil, :all, current_user, true) |     packs = Ammo.list_packs(current_user, show_used: true) | ||||||
|     used_counts = ammo_groups |> ActivityLog.get_used_counts(current_user) |  | ||||||
|     original_counts = ammo_groups |> Ammo.get_original_counts(current_user) |  | ||||||
|     cprs = ammo_groups |> Ammo.get_cprs(current_user) |  | ||||||
|     percentages_remaining = ammo_groups |> Ammo.get_percentages_remaining(current_user) |  | ||||||
|  |  | ||||||
|     ammo_groups = |     used_counts = | ||||||
|       ammo_groups |       ActivityLog.get_grouped_used_counts(current_user, packs: packs, group_by: :pack_id) | ||||||
|       |> Enum.map(fn %{id: ammo_group_id} = ammo_group -> |  | ||||||
|         ammo_group |     original_counts = packs |> Ammo.get_original_counts(current_user) | ||||||
|  |     cprs = packs |> Ammo.get_cprs(current_user) | ||||||
|  |     percentages_remaining = packs |> Ammo.get_percentages_remaining(current_user) | ||||||
|  |  | ||||||
|  |     packs = | ||||||
|  |       packs | ||||||
|  |       |> Enum.map(fn %{id: pack_id} = pack -> | ||||||
|  |         pack | ||||||
|         |> Jason.encode!() |         |> Jason.encode!() | ||||||
|         |> Jason.decode!() |         |> Jason.decode!() | ||||||
|         |> Map.merge(%{ |         |> Map.merge(%{ | ||||||
|           "used_count" => Map.get(used_counts, ammo_group_id), |           "used_count" => Map.get(used_counts, pack_id), | ||||||
|           "percentage_remaining" => Map.fetch!(percentages_remaining, ammo_group_id), |           "percentage_remaining" => Map.fetch!(percentages_remaining, pack_id), | ||||||
|           "original_count" => Map.get(original_counts, ammo_group_id), |           "original_count" => Map.get(original_counts, pack_id), | ||||||
|           "cpr" => Map.get(cprs, ammo_group_id) |           "cpr" => Map.get(cprs, pack_id) | ||||||
|         }) |         }) | ||||||
|       end) |       end) | ||||||
|  |  | ||||||
|     shot_groups = ActivityLog.list_shot_groups(:all, current_user) |     shot_records = ActivityLog.list_shot_records(current_user) | ||||||
|  |  | ||||||
|     containers = |     containers = | ||||||
|       Containers.list_containers(current_user) |       Containers.list_containers(current_user) | ||||||
|       |> Enum.map(fn container -> |       |> Enum.map(fn container -> | ||||||
|         ammo_group_count = container |> Ammo.get_ammo_groups_count_for_container!(current_user) |  | ||||||
|         round_count = container |> Ammo.get_round_count_for_container!(current_user) |  | ||||||
|  |  | ||||||
|         container |         container | ||||||
|         |> Jason.encode!() |         |> Jason.encode!() | ||||||
|         |> Jason.decode!() |         |> Jason.decode!() | ||||||
|         |> Map.merge(%{ |         |> Map.merge(%{ | ||||||
|           "ammo_group_count" => ammo_group_count, |           "pack_count" => Ammo.get_packs_count(current_user, container_id: container.id), | ||||||
|           "round_count" => round_count |           "round_count" => Ammo.get_round_count(current_user, container_id: container.id) | ||||||
|         }) |         }) | ||||||
|       end) |       end) | ||||||
|  |  | ||||||
|     json(conn, %{ |     json(conn, %{ | ||||||
|       user: current_user, |       user: current_user, | ||||||
|       ammo_types: ammo_types, |       types: types, | ||||||
|       ammo_groups: ammo_groups, |       packs: packs, | ||||||
|       shot_groups: shot_groups, |       shot_records: shot_records, | ||||||
|       containers: containers |       containers: containers | ||||||
|     }) |     }) | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -1,11 +0,0 @@ | |||||||
| defmodule CanneryWeb.HomeController do |  | ||||||
|   @moduledoc """ |  | ||||||
|   Controller for home page |  | ||||||
|   """ |  | ||||||
|  |  | ||||||
|   use CanneryWeb, :controller |  | ||||||
|  |  | ||||||
|   def index(conn, _params) do |  | ||||||
|     render(conn, "index.html") |  | ||||||
|   end |  | ||||||
| end |  | ||||||
							
								
								
									
										5
									
								
								lib/cannery_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/cannery_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | defmodule CanneryWeb.HomeHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "home_html/*" | ||||||
|  | end | ||||||
| @@ -3,12 +3,11 @@ defmodule CanneryWeb.UserAuth do | |||||||
|   Functions for user session and authentication |   Functions for user session and authentication | ||||||
|   """ |   """ | ||||||
|  |  | ||||||
|  |   use CanneryWeb, :verified_routes | ||||||
|   import Plug.Conn |   import Plug.Conn | ||||||
|   import Phoenix.Controller |   import Phoenix.Controller | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   alias Cannery.{Accounts, Accounts.User} |   alias Cannery.{Accounts, Accounts.User} | ||||||
|   alias CanneryWeb.HomeLive |  | ||||||
|   alias CanneryWeb.Router.Helpers, as: Routes |  | ||||||
|  |  | ||||||
|   # Make the remember me cookie valid for 60 days. |   # Make the remember me cookie valid for 60 days. | ||||||
|   # If you want bump or reduce this value, also change |   # If you want bump or reduce this value, also change | ||||||
| @@ -39,7 +38,7 @@ defmodule CanneryWeb.UserAuth do | |||||||
|       dgettext("errors", "You must confirm your account and log in to access this page.") |       dgettext("errors", "You must confirm your account and log in to access this page.") | ||||||
|     ) |     ) | ||||||
|     |> maybe_store_return_to() |     |> maybe_store_return_to() | ||||||
|     |> redirect(to: Routes.user_session_path(conn, :new)) |     |> redirect(to: ~p"/users/log_in") | ||||||
|     |> halt() |     |> halt() | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -49,8 +48,7 @@ defmodule CanneryWeb.UserAuth do | |||||||
|  |  | ||||||
|     conn |     conn | ||||||
|     |> renew_session() |     |> renew_session() | ||||||
|     |> put_session(:user_token, token) |     |> put_token_in_session(token) | ||||||
|     |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") |  | ||||||
|     |> maybe_write_remember_me_cookie(token, params) |     |> maybe_write_remember_me_cookie(token, params) | ||||||
|     |> redirect(to: user_return_to || signed_in_path(conn)) |     |> redirect(to: user_return_to || signed_in_path(conn)) | ||||||
|   end |   end | ||||||
| @@ -96,7 +94,7 @@ defmodule CanneryWeb.UserAuth do | |||||||
|   """ |   """ | ||||||
|   def log_out_user(conn) do |   def log_out_user(conn) do | ||||||
|     user_token = get_session(conn, :user_token) |     user_token = get_session(conn, :user_token) | ||||||
|     user_token && Accounts.delete_session_token(user_token) |     user_token && Accounts.delete_user_session_token(user_token) | ||||||
|  |  | ||||||
|     if live_socket_id = get_session(conn, :live_socket_id) do |     if live_socket_id = get_session(conn, :live_socket_id) do | ||||||
|       CanneryWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) |       CanneryWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) | ||||||
| @@ -105,7 +103,7 @@ defmodule CanneryWeb.UserAuth do | |||||||
|     conn |     conn | ||||||
|     |> renew_session() |     |> renew_session() | ||||||
|     |> delete_resp_cookie(@remember_me_cookie) |     |> delete_resp_cookie(@remember_me_cookie) | ||||||
|     |> redirect(to: "/") |     |> redirect(to: ~p"/") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -119,19 +117,110 @@ defmodule CanneryWeb.UserAuth do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp ensure_user_token(conn) do |   defp ensure_user_token(conn) do | ||||||
|     if user_token = get_session(conn, :user_token) do |     if token = get_session(conn, :user_token) do | ||||||
|       {user_token, conn} |       {token, conn} | ||||||
|     else |     else | ||||||
|       conn = fetch_cookies(conn, signed: [@remember_me_cookie]) |       conn = fetch_cookies(conn, signed: [@remember_me_cookie]) | ||||||
|  |  | ||||||
|       if user_token = conn.cookies[@remember_me_cookie] do |       if token = conn.cookies[@remember_me_cookie] do | ||||||
|         {user_token, put_session(conn, :user_token, user_token)} |         {token, put_token_in_session(conn, token)} | ||||||
|       else |       else | ||||||
|         {nil, conn} |         {nil, conn} | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Handles mounting and authenticating the current_user in LiveViews. | ||||||
|  |  | ||||||
|  |   ## `on_mount` arguments | ||||||
|  |  | ||||||
|  |     * `:mount_current_user` - Assigns current_user | ||||||
|  |       to socket assigns based on user_token, or nil if | ||||||
|  |       there's no user_token or no matching user. | ||||||
|  |  | ||||||
|  |     * `:ensure_authenticated` - Authenticates the user from the session, | ||||||
|  |       and assigns the current_user to socket assigns based | ||||||
|  |       on user_token. | ||||||
|  |       Redirects to login page if there's no logged user. | ||||||
|  |  | ||||||
|  |     * `:redirect_if_user_is_authenticated` - Authenticates the user from the session. | ||||||
|  |       Redirects to signed_in_path if there's a logged user. | ||||||
|  |  | ||||||
|  |   ## Examples | ||||||
|  |  | ||||||
|  |   Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate | ||||||
|  |   the current_user: | ||||||
|  |  | ||||||
|  |       defmodule CanneryWeb.PageLive do | ||||||
|  |         use CanneryWeb, :live_view | ||||||
|  |  | ||||||
|  |         on_mount {CanneryWeb.UserAuth, :mount_current_user} | ||||||
|  |         ... | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |   Or use the `live_session` of your router to invoke the on_mount callback: | ||||||
|  |  | ||||||
|  |       live_session :authenticated, on_mount: [{CanneryWeb.UserAuth, :ensure_authenticated}] do | ||||||
|  |         live "/profile", ProfileLive, :index | ||||||
|  |       end | ||||||
|  |   """ | ||||||
|  |   def on_mount(:mount_current_user, _params, session, socket) do | ||||||
|  |     {:cont, mount_current_user(session, socket)} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def on_mount(:ensure_authenticated, _params, session, socket) do | ||||||
|  |     socket = mount_current_user(session, socket) | ||||||
|  |  | ||||||
|  |     if socket.assigns.current_user do | ||||||
|  |       {:cont, socket} | ||||||
|  |     else | ||||||
|  |       error_flash = dgettext("errors", "You must log in to access this page.") | ||||||
|  |  | ||||||
|  |       socket = | ||||||
|  |         socket | ||||||
|  |         |> Phoenix.LiveView.put_flash(:error, error_flash) | ||||||
|  |         |> Phoenix.LiveView.redirect(to: ~p"/users/log_in") | ||||||
|  |  | ||||||
|  |       {:halt, socket} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def on_mount(:ensure_admin, _params, session, socket) do | ||||||
|  |     socket = mount_current_user(session, socket) | ||||||
|  |  | ||||||
|  |     if socket.assigns.current_user && socket.assigns.current_user.role == :admin do | ||||||
|  |       {:cont, socket} | ||||||
|  |     else | ||||||
|  |       error_flash = dgettext("errors", "You must log in as an administrator to access this page.") | ||||||
|  |  | ||||||
|  |       socket = | ||||||
|  |         socket | ||||||
|  |         |> Phoenix.LiveView.put_flash(:error, error_flash) | ||||||
|  |         |> Phoenix.LiveView.redirect(to: ~p"/users/log_in") | ||||||
|  |  | ||||||
|  |       {:halt, socket} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do | ||||||
|  |     socket = mount_current_user(session, socket) | ||||||
|  |  | ||||||
|  |     if socket.assigns.current_user do | ||||||
|  |       {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))} | ||||||
|  |     else | ||||||
|  |       {:cont, socket} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp mount_current_user(session, socket) do | ||||||
|  |     Phoenix.Component.assign_new(socket, :current_user, fn -> | ||||||
|  |       if user_token = session["user_token"] do | ||||||
|  |         Accounts.get_user_by_session_token(user_token) | ||||||
|  |       end | ||||||
|  |     end) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Used for routes that require the user to not be authenticated. |   Used for routes that require the user to not be authenticated. | ||||||
|   """ |   """ | ||||||
| @@ -161,7 +250,7 @@ defmodule CanneryWeb.UserAuth do | |||||||
|         dgettext("errors", "You must confirm your account and log in to access this page.") |         dgettext("errors", "You must confirm your account and log in to access this page.") | ||||||
|       ) |       ) | ||||||
|       |> maybe_store_return_to() |       |> maybe_store_return_to() | ||||||
|       |> redirect(to: Routes.user_session_path(conn, :new)) |       |> redirect(to: ~p"/users/log_in") | ||||||
|       |> halt() |       |> halt() | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| @@ -176,16 +265,34 @@ defmodule CanneryWeb.UserAuth do | |||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "You are not authorized to view this page.")) |       |> put_flash(:error, dgettext("errors", "You are not authorized to view this page.")) | ||||||
|       |> maybe_store_return_to() |       |> maybe_store_return_to() | ||||||
|       |> redirect(to: Routes.live_path(conn, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|       |> halt() |       |> halt() | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def 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 | ||||||
|  |  | ||||||
|  |   def put_user_locale(conn, _opts) do | ||||||
|  |     default = Application.fetch_env!(:gettext, :default_locale) | ||||||
|  |     Gettext.put_locale(default) | ||||||
|  |     conn |> put_session(:locale, default) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp put_token_in_session(conn, token) do | ||||||
|  |     conn | ||||||
|  |     |> put_session(:user_token, token) | ||||||
|  |     |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") | ||||||
|  |   end | ||||||
|  |  | ||||||
|   defp maybe_store_return_to(%{method: "GET"} = conn) do |   defp maybe_store_return_to(%{method: "GET"} = conn) do | ||||||
|     put_session(conn, :user_return_to, current_path(conn)) |     put_session(conn, :user_return_to, current_path(conn)) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp maybe_store_return_to(conn), do: conn |   defp maybe_store_return_to(conn), do: conn | ||||||
|  |  | ||||||
|   defp signed_in_path(_conn), do: "/" |   defp signed_in_path(_conn), do: ~p"/" | ||||||
| end | end | ||||||
|   | |||||||
| @@ -5,14 +5,14 @@ defmodule CanneryWeb.UserConfirmationController do | |||||||
|   alias Cannery.Accounts |   alias Cannery.Accounts | ||||||
|  |  | ||||||
|   def new(conn, _params) do |   def new(conn, _params) do | ||||||
|     render(conn, "new.html", page_title: gettext("Confirm your account")) |     render(conn, :new, page_title: gettext("Confirm your account")) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create(conn, %{"user" => %{"email" => email}}) do |   def create(conn, %{"user" => %{"email" => email}}) do | ||||||
|     if user = Accounts.get_user_by_email(email) do |     if user = Accounts.get_user_by_email(email) do | ||||||
|       Accounts.deliver_user_confirmation_instructions( |       Accounts.deliver_user_confirmation_instructions( | ||||||
|         user, |         user, | ||||||
|         &Routes.user_confirmation_url(conn, :confirm, &1) |         fn token -> url(CanneryWeb.Endpoint, ~p"/users/confirm/#{token}") end | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
|  |  | ||||||
| @@ -22,11 +22,10 @@ defmodule CanneryWeb.UserConfirmationController do | |||||||
|       :info, |       :info, | ||||||
|       dgettext( |       dgettext( | ||||||
|         "prompts", |         "prompts", | ||||||
|         "If your email is in our system and it has not been confirmed yet, " <> |         "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." | ||||||
|           "you will receive an email with instructions shortly." |  | ||||||
|       ) |       ) | ||||||
|     ) |     ) | ||||||
|     |> redirect(to: "/") |     |> redirect(to: ~p"/") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   # Do not log in the user after confirmation to avoid a |   # Do not log in the user after confirmation to avoid a | ||||||
| @@ -36,7 +35,7 @@ defmodule CanneryWeb.UserConfirmationController do | |||||||
|       {:ok, %{email: email}} -> |       {:ok, %{email: email}} -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email)) |         |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email)) | ||||||
|         |> redirect(to: "/") |         |> redirect(to: ~p"/") | ||||||
|  |  | ||||||
|       :error -> |       :error -> | ||||||
|         # If there is a current user and the account was already confirmed, |         # If there is a current user and the account was already confirmed, | ||||||
| @@ -45,7 +44,7 @@ defmodule CanneryWeb.UserConfirmationController do | |||||||
|         # a warning message. |         # a warning message. | ||||||
|         case conn.assigns do |         case conn.assigns do | ||||||
|           %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> |           %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> | ||||||
|             redirect(conn, to: "/") |             redirect(conn, to: ~p"/") | ||||||
|  |  | ||||||
|           %{} -> |           %{} -> | ||||||
|             conn |             conn | ||||||
| @@ -53,7 +52,7 @@ defmodule CanneryWeb.UserConfirmationController do | |||||||
|               :error, |               :error, | ||||||
|               dgettext("errors", "User confirmation link is invalid or it has expired.") |               dgettext("errors", "User confirmation link is invalid or it has expired.") | ||||||
|             ) |             ) | ||||||
|             |> redirect(to: "/") |             |> redirect(to: ~p"/") | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								lib/cannery_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/cannery_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | defmodule CanneryWeb.UserConfirmationHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |   alias Cannery.Accounts | ||||||
|  |  | ||||||
|  |   embed_templates "user_confirmation_html/*" | ||||||
|  | end | ||||||
| @@ -7,7 +7,7 @@ | |||||||
|     :let={f} |     :let={f} | ||||||
|     for={%{}} |     for={%{}} | ||||||
|     as={:user} |     as={:user} | ||||||
|     action={Routes.user_confirmation_path(@conn, :create)} |     action={~p"/users/confirm"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> |     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> | ||||||
| @@ -21,14 +21,10 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex flex-row justify-center items-center space-x-4"> |   <div class="flex flex-row justify-center items-center space-x-4"> | ||||||
|     <.link |     <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary"> | ||||||
|       :if={Accounts.allow_registration?()} |  | ||||||
|       href={Routes.user_registration_path(@conn, :new)} |  | ||||||
|       class="btn btn-primary" |  | ||||||
|     > |  | ||||||
|       <%= dgettext("actions", "Register") %> |       <%= dgettext("actions", "Register") %> | ||||||
|     </.link> |     </.link> | ||||||
|     <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/log_in"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Log in") %> |       <%= dgettext("actions", "Log in") %> | ||||||
|     </.link> |     </.link> | ||||||
|   </div> |   </div> | ||||||
| @@ -2,7 +2,6 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|   use CanneryWeb, :controller |   use CanneryWeb, :controller | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   alias Cannery.{Accounts, Accounts.Invites} |   alias Cannery.{Accounts, Accounts.Invites} | ||||||
|   alias CanneryWeb.{Endpoint, HomeLive} |  | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|  |  | ||||||
|   def new(conn, %{"invite" => invite_token}) do |   def new(conn, %{"invite" => invite_token}) do | ||||||
| @@ -11,7 +10,7 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|     else |     else | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) |       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -21,13 +20,13 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|     else |     else | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) |       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   # renders new user registration page |   # renders new user registration page | ||||||
|   defp render_new(conn, invite_token \\ nil) do |   defp render_new(conn, invite_token \\ nil) do | ||||||
|     render(conn, "new.html", |     render(conn, :new, | ||||||
|       changeset: Accounts.change_user_registration(), |       changeset: Accounts.change_user_registration(), | ||||||
|       invite_token: invite_token, |       invite_token: invite_token, | ||||||
|       page_title: gettext("Register") |       page_title: gettext("Register") | ||||||
| @@ -40,7 +39,7 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|     else |     else | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) |       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -50,7 +49,7 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|     else |     else | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) |       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -59,20 +58,20 @@ defmodule CanneryWeb.UserRegistrationController do | |||||||
|       {:ok, user} -> |       {:ok, user} -> | ||||||
|         Accounts.deliver_user_confirmation_instructions( |         Accounts.deliver_user_confirmation_instructions( | ||||||
|           user, |           user, | ||||||
|           &Routes.user_confirmation_url(conn, :confirm, &1) |           fn token -> url(CanneryWeb.Endpoint, ~p"/users/confirm/#{token}") end | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account")) |         |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account")) | ||||||
|         |> redirect(to: Routes.user_session_path(Endpoint, :new)) |         |> redirect(to: ~p"/users/log_in") | ||||||
|  |  | ||||||
|       {:error, :invalid_token} -> |       {:error, :invalid_token} -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) |         |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||||
|         |> redirect(to: Routes.live_path(Endpoint, HomeLive)) |         |> redirect(to: ~p"/") | ||||||
|  |  | ||||||
|       {:error, %Changeset{} = changeset} -> |       {:error, %Changeset{} = changeset} -> | ||||||
|         conn |> render("new.html", changeset: changeset, invite_token: invite_token) |         conn |> render(:new, changeset: changeset, invite_token: invite_token) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								lib/cannery_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/cannery_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | defmodule CanneryWeb.UserRegistrationHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "user_registration_html/*" | ||||||
|  | end | ||||||
| @@ -6,10 +6,10 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@changeset} |     for={@changeset} | ||||||
|     action={Routes.user_registration_path(@conn, :create)} |     action={~p"/users/register"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3"> |     <p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3"> | ||||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> |       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||||
|     </p> |     </p> | ||||||
| 
 | 
 | ||||||
| @@ -40,10 +40,10 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex flex-row justify-center items-center space-x-4"> |   <div class="flex flex-row justify-center items-center space-x-4"> | ||||||
|     <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/log_in"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Log in") %> |       <%= dgettext("actions", "Log in") %> | ||||||
|     </.link> |     </.link> | ||||||
|     <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/reset_password"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Forgot your password?") %> |       <%= dgettext("actions", "Forgot your password?") %> | ||||||
|     </.link> |     </.link> | ||||||
|   </div> |   </div> | ||||||
| @@ -6,14 +6,14 @@ 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", page_title: gettext("Forgot your password?")) |     render(conn, :new, page_title: gettext("Forgot your password?")) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create(conn, %{"user" => %{"email" => email}}) do |   def create(conn, %{"user" => %{"email" => email}}) do | ||||||
|     if user = Accounts.get_user_by_email(email) do |     if user = Accounts.get_user_by_email(email) do | ||||||
|       Accounts.deliver_user_reset_password_instructions( |       Accounts.deliver_user_reset_password_instructions( | ||||||
|         user, |         user, | ||||||
|         &Routes.user_reset_password_url(conn, :edit, &1) |         fn token -> url(CanneryWeb.Endpoint, ~p"/users/reset_password/#{token}") end | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
|  |  | ||||||
| @@ -23,15 +23,14 @@ defmodule CanneryWeb.UserResetPasswordController do | |||||||
|       :info, |       :info, | ||||||
|       dgettext( |       dgettext( | ||||||
|         "prompts", |         "prompts", | ||||||
|         "If your email is in our system, you will receive instructions to " <> |         "If your email is in our system, you will receive instructions to reset your password shortly." | ||||||
|           "reset your password shortly." |  | ||||||
|       ) |       ) | ||||||
|     ) |     ) | ||||||
|     |> redirect(to: "/") |     |> redirect(to: ~p"/") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def edit(conn, _params) do |   def edit(conn, _params) do | ||||||
|     render(conn, "edit.html", |     render(conn, :edit, | ||||||
|       changeset: Accounts.change_user_password(conn.assigns.user), |       changeset: Accounts.change_user_password(conn.assigns.user), | ||||||
|       page_title: gettext("Reset your password") |       page_title: gettext("Reset your password") | ||||||
|     ) |     ) | ||||||
| @@ -44,10 +43,10 @@ defmodule CanneryWeb.UserResetPasswordController do | |||||||
|       {:ok, _} -> |       {:ok, _} -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "Password reset successfully.")) |         |> put_flash(:info, dgettext("prompts", "Password reset successfully.")) | ||||||
|         |> redirect(to: Routes.user_session_path(conn, :new)) |         |> redirect(to: ~p"/users/log_in") | ||||||
|  |  | ||||||
|       {:error, changeset} -> |       {:error, changeset} -> | ||||||
|         render(conn, "edit.html", changeset: changeset) |         render(conn, :edit, changeset: changeset) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -62,7 +61,7 @@ defmodule CanneryWeb.UserResetPasswordController do | |||||||
|         :error, |         :error, | ||||||
|         dgettext("errors", "Reset password link is invalid or it has expired.") |         dgettext("errors", "Reset password link is invalid or it has expired.") | ||||||
|       ) |       ) | ||||||
|       |> redirect(to: "/") |       |> redirect(to: ~p"/") | ||||||
|       |> halt() |       |> halt() | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								lib/cannery_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/cannery_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | defmodule CanneryWeb.UserResetPasswordHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |   alias Cannery.Accounts | ||||||
|  |  | ||||||
|  |   embed_templates "user_reset_password_html/*" | ||||||
|  | end | ||||||
| @@ -6,10 +6,10 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@changeset} |     for={@changeset} | ||||||
|     action={Routes.user_reset_password_path(@conn, :update, @token)} |     action={~p"/users/reset_password/#{@token}"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3"> |     <p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3"> | ||||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> |       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||||
|     </p> |     </p> | ||||||
| 
 | 
 | ||||||
| @@ -34,14 +34,10 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex flex-row justify-center items-center space-x-4"> |   <div class="flex flex-row justify-center items-center space-x-4"> | ||||||
|     <.link |     <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary"> | ||||||
|       :if={Accounts.allow_registration?()} |  | ||||||
|       href={Routes.user_registration_path(@conn, :new)} |  | ||||||
|       class="btn btn-primary" |  | ||||||
|     > |  | ||||||
|       <%= dgettext("actions", "Register") %> |       <%= dgettext("actions", "Register") %> | ||||||
|     </.link> |     </.link> | ||||||
|     <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/log_in"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Log in") %> |       <%= dgettext("actions", "Log in") %> | ||||||
|     </.link> |     </.link> | ||||||
|   </div> |   </div> | ||||||
| @@ -7,7 +7,7 @@ | |||||||
|     :let={f} |     :let={f} | ||||||
|     for={%{}} |     for={%{}} | ||||||
|     as={:user} |     as={:user} | ||||||
|     action={Routes.user_reset_password_path(@conn, :create)} |     action={~p"/users/reset_password"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> |     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> | ||||||
| @@ -21,14 +21,10 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex flex-row justify-center items-center space-x-4"> |   <div class="flex flex-row justify-center items-center space-x-4"> | ||||||
|     <.link |     <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary"> | ||||||
|       :if={Accounts.allow_registration?()} |  | ||||||
|       href={Routes.user_registration_path(@conn, :new)} |  | ||||||
|       class="btn btn-primary" |  | ||||||
|     > |  | ||||||
|       <%= dgettext("actions", "Register") %> |       <%= dgettext("actions", "Register") %> | ||||||
|     </.link> |     </.link> | ||||||
|     <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/log_in"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Log in") %> |       <%= dgettext("actions", "Log in") %> | ||||||
|     </.link> |     </.link> | ||||||
|   </div> |   </div> | ||||||
| @@ -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, page_title: gettext("Log in")) |     render(conn, :new, error_message: nil, page_title: gettext("Log in")) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create(conn, %{"user" => user_params}) do |   def create(conn, %{"user" => user_params}) do | ||||||
| @@ -14,7 +14,7 @@ defmodule CanneryWeb.UserSessionController do | |||||||
|     if user = Accounts.get_user_by_email_and_password(email, password) do |     if user = Accounts.get_user_by_email_and_password(email, password) do | ||||||
|       UserAuth.log_in_user(conn, user, user_params) |       UserAuth.log_in_user(conn, user, user_params) | ||||||
|     else |     else | ||||||
|       render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password")) |       render(conn, :new, error_message: dgettext("errors", "Invalid email or password")) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								lib/cannery_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/cannery_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | defmodule CanneryWeb.UserSessionHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |   alias Cannery.Accounts | ||||||
|  |  | ||||||
|  |   embed_templates "user_session_html/*" | ||||||
|  | end | ||||||
| @@ -6,8 +6,8 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@conn} |     for={@conn} | ||||||
|     action={Routes.user_session_path(@conn, :create)} |     action={~p"/users/log_in"} | ||||||
|     as="user" |     as={:user} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <p :if={@error_message} class="alert alert-danger col-span-3"> |     <p :if={@error_message} class="alert alert-danger col-span-3"> | ||||||
| @@ -15,10 +15,18 @@ | |||||||
|     </p> |     </p> | ||||||
| 
 | 
 | ||||||
|     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> |     <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> |     <%= email_input(f, :email, | ||||||
|  |       autocomplete: :email, | ||||||
|  |       class: "input input-primary col-span-2", | ||||||
|  |       required: true | ||||||
|  |     ) %> | ||||||
| 
 | 
 | ||||||
|     <%= label(f, :password, gettext("Password"), class: "title text-lg text-primary-600") %> |     <%= label(f, :password, gettext("Password"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %> |     <%= password_input(f, :password, | ||||||
|  |       autocomplete: "current-password", | ||||||
|  |       class: "input input-primary col-span-2", | ||||||
|  |       required: true | ||||||
|  |     ) %> | ||||||
| 
 | 
 | ||||||
|     <%= label(f, :remember_me, gettext("Keep me logged in for 60 days"), |     <%= label(f, :remember_me, gettext("Keep me logged in for 60 days"), | ||||||
|       class: "title text-lg text-primary-600" |       class: "title text-lg text-primary-600" | ||||||
| @@ -31,14 +39,10 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex flex-row justify-center items-center space-x-4"> |   <div class="flex flex-row justify-center items-center space-x-4"> | ||||||
|     <.link |     <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary"> | ||||||
|       :if={Accounts.allow_registration?()} |  | ||||||
|       href={Routes.user_registration_path(@conn, :new)} |  | ||||||
|       class="btn btn-primary" |  | ||||||
|     > |  | ||||||
|       <%= dgettext("actions", "Register") %> |       <%= dgettext("actions", "Register") %> | ||||||
|     </.link> |     </.link> | ||||||
|     <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary"> |     <.link href={~p"/users/reset_password"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Forgot your password?") %> |       <%= dgettext("actions", "Forgot your password?") %> | ||||||
|     </.link> |     </.link> | ||||||
|   </div> |   </div> | ||||||
| @@ -2,12 +2,12 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|   use CanneryWeb, :controller |   use CanneryWeb, :controller | ||||||
|   import CanneryWeb.Gettext |   import CanneryWeb.Gettext | ||||||
|   alias Cannery.Accounts |   alias Cannery.Accounts | ||||||
|   alias CanneryWeb.{HomeLive, UserAuth} |   alias CanneryWeb.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", page_title: gettext("Settings")) |     render(conn, :edit, page_title: gettext("Settings")) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def update(%{assigns: %{current_user: user}} = conn, %{ |   def update(%{assigns: %{current_user: user}} = conn, %{ | ||||||
| @@ -20,7 +20,7 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|         Accounts.deliver_update_email_instructions( |         Accounts.deliver_update_email_instructions( | ||||||
|           applied_user, |           applied_user, | ||||||
|           user.email, |           user.email, | ||||||
|           &Routes.user_settings_url(conn, :confirm_email, &1) |           fn token -> url(CanneryWeb.Endpoint, ~p"/users/settings/confirm_email/#{token}") end | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         conn |         conn | ||||||
| @@ -31,10 +31,10 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|             "A link to confirm your email change has been sent to the new address." |             "A link to confirm your email change has been sent to the new address." | ||||||
|           ) |           ) | ||||||
|         ) |         ) | ||||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) |         |> redirect(to: ~p"/users/settings") | ||||||
|  |  | ||||||
|       {:error, changeset} -> |       {:error, changeset} -> | ||||||
|         conn |> render("edit.html", email_changeset: changeset) |         conn |> render(:edit, email_changeset: changeset) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -47,11 +47,11 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|       {:ok, user} -> |       {:ok, user} -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "Password updated successfully.")) |         |> put_flash(:info, dgettext("prompts", "Password updated successfully.")) | ||||||
|         |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) |         |> put_session(:user_return_to, ~p"/users/settings") | ||||||
|         |> UserAuth.log_in_user(user) |         |> UserAuth.log_in_user(user) | ||||||
|  |  | ||||||
|       {:error, changeset} -> |       {:error, changeset} -> | ||||||
|         conn |> render("edit.html", password_changeset: changeset) |         conn |> render(:edit, password_changeset: changeset) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -63,10 +63,10 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|       {:ok, _user} -> |       {:ok, _user} -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "Language updated successfully.")) |         |> put_flash(:info, dgettext("prompts", "Language updated successfully.")) | ||||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) |         |> redirect(to: ~p"/users/settings") | ||||||
|  |  | ||||||
|       {:error, changeset} -> |       {:error, changeset} -> | ||||||
|         conn |> render("edit.html", locale_changeset: changeset) |         conn |> render(:edit, locale_changeset: changeset) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -75,7 +75,7 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|       :ok -> |       :ok -> | ||||||
|         conn |         conn | ||||||
|         |> put_flash(:info, dgettext("prompts", "Email changed successfully.")) |         |> put_flash(:info, dgettext("prompts", "Email changed successfully.")) | ||||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) |         |> redirect(to: ~p"/users/settings") | ||||||
|  |  | ||||||
|       :error -> |       :error -> | ||||||
|         conn |         conn | ||||||
| @@ -83,7 +83,7 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|           :error, |           :error, | ||||||
|           dgettext("errors", "Email change link is invalid or it has expired.") |           dgettext("errors", "Email change link is invalid or it has expired.") | ||||||
|         ) |         ) | ||||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) |         |> redirect(to: ~p"/users/settings") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -93,11 +93,11 @@ defmodule CanneryWeb.UserSettingsController do | |||||||
|  |  | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("prompts", "Your account has been deleted")) |       |> put_flash(:error, dgettext("prompts", "Your account has been deleted")) | ||||||
|       |> redirect(to: Routes.live_path(conn, HomeLive)) |       |> redirect(to: ~p"/") | ||||||
|     else |     else | ||||||
|       conn |       conn | ||||||
|       |> put_flash(:error, dgettext("errors", "Unable to delete user")) |       |> put_flash(:error, dgettext("errors", "Unable to delete user")) | ||||||
|       |> redirect(to: Routes.user_settings_path(conn, :edit)) |       |> redirect(to: ~p"/users/settings") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								lib/cannery_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/cannery_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | defmodule CanneryWeb.UserSettingsHTML do | ||||||
|  |   use CanneryWeb, :html | ||||||
|  |  | ||||||
|  |   embed_templates "user_settings_html/*" | ||||||
|  | end | ||||||
| @@ -8,7 +8,7 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@email_changeset} |     for={@email_changeset} | ||||||
|     action={Routes.user_settings_path(@conn, :update)} |     action={~p"/users/settings"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <h3 class="title text-primary-600 text-lg text-center col-span-3"> |     <h3 class="title text-primary-600 text-lg text-center col-span-3"> | ||||||
| @@ -16,7 +16,7 @@ | |||||||
|     </h3> |     </h3> | ||||||
| 
 | 
 | ||||||
|     <div |     <div | ||||||
|       :if={@email_changeset.action && not @email_changeset.valid?()} |       :if={@email_changeset.action && not @email_changeset.valid?} | ||||||
|       class="alert alert-danger col-span-3" |       class="alert alert-danger col-span-3" | ||||||
|     > |     > | ||||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> |       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||||
| @@ -50,7 +50,7 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@password_changeset} |     for={@password_changeset} | ||||||
|     action={Routes.user_settings_path(@conn, :update)} |     action={~p"/users/settings"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <h3 class="title text-primary-600 text-lg text-center col-span-3"> |     <h3 class="title text-primary-600 text-lg text-center col-span-3"> | ||||||
| @@ -58,7 +58,7 @@ | |||||||
|     </h3> |     </h3> | ||||||
| 
 | 
 | ||||||
|     <div |     <div | ||||||
|       :if={@password_changeset.action && not @password_changeset.valid?()} |       :if={@password_changeset.action && not @password_changeset.valid?} | ||||||
|       class="alert alert-danger col-span-3" |       class="alert alert-danger col-span-3" | ||||||
|     > |     > | ||||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> |       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||||
| @@ -104,7 +104,7 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@locale_changeset} |     for={@locale_changeset} | ||||||
|     action={Routes.user_settings_path(@conn, :update)} |     action={~p"/users/settings"} | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <%= label(f, :locale, dgettext("actions", "Change Language"), |     <%= label(f, :locale, dgettext("actions", "Change Language"), | ||||||
| @@ -112,7 +112,7 @@ | |||||||
|     ) %> |     ) %> | ||||||
| 
 | 
 | ||||||
|     <div |     <div | ||||||
|       :if={@locale_changeset.action && not @locale_changeset.valid?()} |       :if={@locale_changeset.action && not @locale_changeset.valid?} | ||||||
|       class="alert alert-danger col-span-3" |       class="alert alert-danger col-span-3" | ||||||
|     > |     > | ||||||
|       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> |       <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||||
| @@ -142,17 +142,13 @@ | |||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
| 
 | 
 | ||||||
|   <div class="flex justify-center items-center"> |   <div class="flex justify-center items-center"> | ||||||
|     <.link |     <.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank"> | ||||||
|       href={Routes.export_path(@conn, :export, :json)} |  | ||||||
|       class="mx-4 my-2 btn btn-primary" |  | ||||||
|       target="_blank" |  | ||||||
|     > |  | ||||||
|       <%= dgettext("actions", "Export Data as JSON") %> |       <%= dgettext("actions", "Export Data as JSON") %> | ||||||
|     </.link> |     </.link> | ||||||
| 
 | 
 | ||||||
|     <.link |     <.link | ||||||
|       href={Routes.user_settings_path(@conn, :delete, @current_user)} |       href={~p"/users/settings/#{@current_user}"} | ||||||
|       method={:delete} |       method="delete" | ||||||
|       class="mx-4 my-2 btn btn-alert" |       class="mx-4 my-2 btn btn-alert" | ||||||
|       data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")} |       data-confirm={dgettext("prompts", "Are you sure you want to delete your account?")} | ||||||
|     > |     > | ||||||
| @@ -20,7 +20,7 @@ defmodule CanneryWeb.Endpoint do | |||||||
|     at: "/", |     at: "/", | ||||||
|     from: :cannery, |     from: :cannery, | ||||||
|     gzip: false, |     gzip: false, | ||||||
|     only: ~w(css fonts images js favicon.ico robots.txt) |     only: CanneryWeb.static_paths() | ||||||
|  |  | ||||||
|   # Code reloading can be explicitly enabled under the |   # Code reloading can be explicitly enabled under the | ||||||
|   # :code_reloader configuration of your endpoint. |   # :code_reloader configuration of your endpoint. | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ defmodule CanneryWeb.ErrorHelpers do | |||||||
|   Conveniences for translating and building error messages. |   Conveniences for translating and building error messages. | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use Phoenix.HTML |   use PhoenixHTMLHelpers | ||||||
|   import Phoenix.Component |   import Phoenix.{Component, HTML.Form} | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|   alias Phoenix.{HTML.Form, LiveView.Rendered} |   alias Phoenix.{HTML.Form, LiveView.Rendered} | ||||||
| 
 | 
 | ||||||
| @@ -65,7 +65,7 @@ defmodule CanneryWeb.ErrorHelpers do | |||||||
|     changeset |     changeset | ||||||
|     |> changeset_error_map() |     |> changeset_error_map() | ||||||
|     |> Enum.map_join(". ", fn {key, errors} -> |     |> Enum.map_join(". ", fn {key, errors} -> | ||||||
|       "#{key |> humanize()}: #{errors |> Enum.join(", ")}" |       "#{key |> Phoenix.Naming.humanize()}: #{errors |> Enum.join(", ")}" | ||||||
|     end) |     end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| defmodule CanneryWeb.ViewHelpers do | defmodule CanneryWeb.HTMLHelpers do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Contains common helpers that can be used in liveviews and regular views. These |   Contains common helpers that are used for rendering | ||||||
|   are automatically imported into any Phoenix View using `use CanneryWeb, |  | ||||||
|   :view` |  | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use Phoenix.Component |   use Phoenix.Component | ||||||
| @@ -1,114 +0,0 @@ | |||||||
| defmodule CanneryWeb.AmmoTypeLive.Index do |  | ||||||
|   @moduledoc """ |  | ||||||
|   Liveview for showing a Cannery.Ammo.AmmoType index |  | ||||||
|   """ |  | ||||||
|  |  | ||||||
|   use CanneryWeb, :live_view |  | ||||||
|   alias Cannery.{Ammo, Ammo.AmmoType} |  | ||||||
|  |  | ||||||
|   @impl true |  | ||||||
|   def mount(%{"search" => search}, _session, socket) do |  | ||||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: search) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def mount(_params, _session, socket) do |  | ||||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: nil) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   @impl true |  | ||||||
|   def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do |  | ||||||
|     {:noreply, apply_action(socket, live_action, params)} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   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 |  | ||||||
|     |> assign( |  | ||||||
|       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 |  | ||||||
|  |  | ||||||
|   defp apply_action(socket, :new, _params) do |  | ||||||
|     socket |  | ||||||
|     |> assign( |  | ||||||
|       page_title: gettext("New Ammo type"), |  | ||||||
|       ammo_type: %AmmoType{} |  | ||||||
|     ) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   defp apply_action(socket, :index, _params) do |  | ||||||
|     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 |  | ||||||
|  |  | ||||||
|   @impl true |  | ||||||
|   def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do |  | ||||||
|     %{name: name} = Ammo.get_ammo_type!(id, current_user) |> Ammo.delete_ammo_type!(current_user) |  | ||||||
|     prompt = dgettext("prompts", "%{name} deleted succesfully", name: name) |  | ||||||
|     {:noreply, socket |> put_flash(:info, prompt) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   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 |  | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do |  | ||||||
|     {:noreply, socket |> push_patch(to: Routes.ammo_type_index_path(Endpoint, :index))} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do |  | ||||||
|     search_path = Routes.ammo_type_index_path(Endpoint, :search, search_term) |  | ||||||
|     {:noreply, socket |> push_patch(to: search_path)} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do |  | ||||||
|     {:noreply, socket |> assign(:type, :rifle) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do |  | ||||||
|     {:noreply, socket |> assign(:type, :shotgun) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do |  | ||||||
|     {:noreply, socket |> assign(:type, :pistol) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do |  | ||||||
|     {:noreply, socket |> assign(:type, :all) |> list_ammo_types()} |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   defp list_ammo_types( |  | ||||||
|          %{assigns: %{type: type, search: search, current_user: current_user}} = socket |  | ||||||
|        ) do |  | ||||||
|     socket |  | ||||||
|     |> assign( |  | ||||||
|       ammo_types: Ammo.list_ammo_types(search, current_user, type), |  | ||||||
|       ammo_types_count: Ammo.get_ammo_types_count!(current_user) |  | ||||||
|     ) |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @@ -12,7 +12,7 @@ | |||||||
|     phx-submit="save" |     phx-submit="save" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       :if={@changeset.action && not @changeset.valid?()} |       :if={@changeset.action && not @changeset.valid?} | ||||||
|       class="invalid-feedback col-span-3 text-center" |       class="invalid-feedback col-span-3 text-center" | ||||||
|     > |     > | ||||||
|       <%= changeset_errors(@changeset) %> |       <%= changeset_errors(@changeset) %> | ||||||
| @@ -31,7 +31,6 @@ | |||||||
|       id: "container-form-desc", |       id: "container-form-desc", | ||||||
|       class: "input input-primary col-span-2", |       class: "input input-primary col-span-2", | ||||||
|       placeholder: gettext("Metal ammo can with the anime girl sticker"), |       placeholder: gettext("Metal ammo can with the anime girl sticker"), | ||||||
|       phx_hook: "MaintainAttrs", |  | ||||||
|       phx_update: "ignore" |       phx_update: "ignore" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :desc, "col-span-3 text-center") %> |     <%= error_tag(f, :desc, "col-span-3 text-center") %> | ||||||
| @@ -49,7 +48,6 @@ | |||||||
|       id: "container-form-location", |       id: "container-form-location", | ||||||
|       class: "input input-primary col-span-2", |       class: "input input-primary col-span-2", | ||||||
|       placeholder: gettext("On the bookshelf"), |       placeholder: gettext("On the bookshelf"), | ||||||
|       phx_hook: "MaintainAttrs", |  | ||||||
|       phx_update: "ignore" |       phx_update: "ignore" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :location, "col-span-3 text-center") %> |     <%= error_tag(f, :location, "col-span-3 text-center") %> | ||||||
|   | |||||||
| @@ -79,15 +79,15 @@ defmodule CanneryWeb.ContainerLive.Index do | |||||||
|               prompt = dgettext("prompts", "%{name} has been deleted", name: container_name) |               prompt = dgettext("prompts", "%{name} has been deleted", name: container_name) | ||||||
|               socket |> put_flash(:info, prompt) |> display_containers() |               socket |> put_flash(:info, prompt) |> display_containers() | ||||||
|  |  | ||||||
|             {:error, %{action: :delete, errors: [ammo_groups: _error], valid?: false} = changeset} -> |             {:error, %{action: :delete, errors: [packs: _error], valid?: false} = changeset} -> | ||||||
|               ammo_groups_error = changeset |> changeset_errors(:ammo_groups) |> Enum.join(", ") |               packs_error = changeset |> changeset_errors(:packs) |> Enum.join(", ") | ||||||
|  |  | ||||||
|               prompt = |               prompt = | ||||||
|                 dgettext( |                 dgettext( | ||||||
|                   "errors", |                   "errors", | ||||||
|                   "Could not delete %{name}: %{error}", |                   "Could not delete %{name}: %{error}", | ||||||
|                   name: changeset |> Changeset.get_field(:name, "container"), |                   name: changeset |> Changeset.get_field(:name, "container"), | ||||||
|                   error: ammo_groups_error |                   error: packs_error | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|               socket |> put_flash(:error, prompt) |               socket |> put_flash(:error, prompt) | ||||||
| @@ -105,15 +105,14 @@ defmodule CanneryWeb.ContainerLive.Index do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do | ||||||
|     {:noreply, socket |> push_patch(to: Routes.container_index_path(Endpoint, :index))} |     {:noreply, socket |> push_patch(to: ~p"/containers")} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do | ||||||
|     {:noreply, |     {:noreply, socket |> push_patch(to: ~p"/containers/search/#{search_term}")} | ||||||
|      socket |> push_patch(to: Routes.container_index_path(Endpoint, :search, search_term))} |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do |   defp display_containers(%{assigns: %{search: search, current_user: current_user}} = socket) do | ||||||
|     socket |> assign(:containers, Containers.list_containers(search, current_user)) |     socket |> assign(:containers, Containers.list_containers(current_user, search: search)) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -9,11 +9,11 @@ | |||||||
|       <%= display_emoji("😔") %> |       <%= display_emoji("😔") %> | ||||||
|     </h2> |     </h2> | ||||||
|  |  | ||||||
|     <.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary"> |     <.link patch={~p"/containers/new"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Add your first container!") %> |       <%= dgettext("actions", "Add your first container!") %> | ||||||
|     </.link> |     </.link> | ||||||
|   <% else %> |   <% else %> | ||||||
|     <.link patch={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary"> |     <.link patch={~p"/containers/new"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "New Container") %> |       <%= dgettext("actions", "New Container") %> | ||||||
|     </.link> |     </.link> | ||||||
|  |  | ||||||
| @@ -51,7 +51,7 @@ | |||||||
|       <%= if @view_table do %> |       <%= if @view_table do %> | ||||||
|         <.live_component |         <.live_component | ||||||
|           module={CanneryWeb.Components.ContainerTableComponent} |           module={CanneryWeb.Components.ContainerTableComponent} | ||||||
|           id="containers_index_table" |           id="containers-index-table" | ||||||
|           action={@live_action} |           action={@live_action} | ||||||
|           containers={@containers} |           containers={@containers} | ||||||
|           current_user={@current_user} |           current_user={@current_user} | ||||||
| @@ -59,7 +59,7 @@ | |||||||
|           <:tag_actions :let={container}> |           <:tag_actions :let={container}> | ||||||
|             <div class="mx-4 my-2"> |             <div class="mx-4 my-2"> | ||||||
|               <.link |               <.link | ||||||
|                 patch={Routes.container_index_path(Endpoint, :edit_tags, container)} |                 patch={~p"/containers/edit_tags/#{container}"} | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Tag %{container_name}", container_name: container.name) |                   dgettext("actions", "Tag %{container_name}", container_name: container.name) | ||||||
| @@ -71,7 +71,7 @@ | |||||||
|           </:tag_actions> |           </:tag_actions> | ||||||
|           <:actions :let={container}> |           <:actions :let={container}> | ||||||
|             <.link |             <.link | ||||||
|               patch={Routes.container_index_path(Endpoint, :edit, container)} |               patch={~p"/containers/edit/#{container}"} | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Edit %{container_name}", container_name: container.name) |                 dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||||
| @@ -81,7 +81,7 @@ | |||||||
|             </.link> |             </.link> | ||||||
|  |  | ||||||
|             <.link |             <.link | ||||||
|               patch={Routes.container_index_path(Endpoint, :clone, container)} |               patch={~p"/containers/clone/#{container}"} | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Clone %{container_name}", container_name: container.name) |                 dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||||
| @@ -118,7 +118,7 @@ | |||||||
|             <:tag_actions> |             <:tag_actions> | ||||||
|               <div class="mx-4 my-2"> |               <div class="mx-4 my-2"> | ||||||
|                 <.link |                 <.link | ||||||
|                   patch={Routes.container_index_path(Endpoint, :edit_tags, container)} |                   patch={~p"/containers/edit_tags/#{container}"} | ||||||
|                   class="text-primary-600 link" |                   class="text-primary-600 link" | ||||||
|                   aria-label={ |                   aria-label={ | ||||||
|                     dgettext("actions", "Tag %{container_name}", container_name: container.name) |                     dgettext("actions", "Tag %{container_name}", container_name: container.name) | ||||||
| @@ -129,7 +129,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </:tag_actions> |             </:tag_actions> | ||||||
|             <.link |             <.link | ||||||
|               patch={Routes.container_index_path(Endpoint, :edit, container)} |               patch={~p"/containers/edit/#{container}"} | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Edit %{container_name}", container_name: container.name) |                 dgettext("actions", "Edit %{container_name}", container_name: container.name) | ||||||
| @@ -139,7 +139,7 @@ | |||||||
|             </.link> |             </.link> | ||||||
|  |  | ||||||
|             <.link |             <.link | ||||||
|               patch={Routes.container_index_path(Endpoint, :clone, container)} |               patch={~p"/containers/clone/#{container}"} | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Clone %{container_name}", container_name: container.name) |                 dgettext("actions", "Clone %{container_name}", container_name: container.name) | ||||||
| @@ -173,26 +173,26 @@ | |||||||
|  |  | ||||||
| <%= case @live_action do %> | <%= case @live_action do %> | ||||||
|   <% modifying when modifying in [:new, :edit, :clone] -> %> |   <% modifying when modifying in [:new, :edit, :clone] -> %> | ||||||
|     <.modal return_to={Routes.container_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/containers"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.ContainerLive.FormComponent} |         module={CanneryWeb.ContainerLive.FormComponent} | ||||||
|         id={@container.id || :new} |         id={@container.id || :new} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         container={@container} |         container={@container} | ||||||
|         return_to={Routes.container_index_path(Endpoint, :index)} |         return_to={~p"/containers"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :edit_tags -> %> |   <% :edit_tags -> %> | ||||||
|     <.modal return_to={Routes.container_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/containers"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.ContainerLive.EditTagsComponent} |         module={CanneryWeb.ContainerLive.EditTagsComponent} | ||||||
|         id={@container.id} |         id={@container.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         container={@container} |         container={@container} | ||||||
|         current_path={Routes.container_index_path(Endpoint, :edit_tags, @container)} |         current_path={~p"/containers/edit_tags/#{@container}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   | |||||||
| @@ -5,19 +5,18 @@ defmodule CanneryWeb.ContainerLive.Show do | |||||||
|  |  | ||||||
|   use CanneryWeb, :live_view |   use CanneryWeb, :live_view | ||||||
|   alias Cannery.{Accounts.User, ActivityLog, Ammo, Containers, Containers.Container} |   alias Cannery.{Accounts.User, ActivityLog, Ammo, Containers, Containers.Container} | ||||||
|   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), |   def mount(_params, _session, socket), | ||||||
|     do: {:ok, socket |> assign(type: :all, view_table: true)} |     do: {:ok, socket |> assign(class: :all, view_table: true)} | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   def handle_params(%{"id" => id}, _session, %{assigns: %{current_user: current_user}} = socket) do |   def handle_params(%{"id" => id}, _session, %{assigns: %{current_user: current_user}} = socket) do | ||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(view_table: true) |       |> assign(:view_table, true) | ||||||
|       |> render_container(id, current_user) |       |> render_container(id, current_user) | ||||||
|  |  | ||||||
|     {:noreply, socket} |     {:noreply, socket} | ||||||
| @@ -59,18 +58,15 @@ defmodule CanneryWeb.ContainerLive.Show do | |||||||
|       |> case do |       |> case do | ||||||
|         {:ok, %{name: container_name}} -> |         {:ok, %{name: container_name}} -> | ||||||
|           prompt = dgettext("prompts", "%{name} has been deleted", name: container_name) |           prompt = dgettext("prompts", "%{name} has been deleted", name: container_name) | ||||||
|  |           socket |> put_flash(:info, prompt) |> push_navigate(to: ~p"/containers") | ||||||
|  |  | ||||||
|           socket |         {:error, %{action: :delete, errors: [packs: _error], valid?: false} = changeset} -> | ||||||
|           |> put_flash(:info, prompt) |           packs_error = changeset |> changeset_errors(:packs) |> Enum.join(", ") | ||||||
|           |> push_navigate(to: Routes.container_index_path(socket, :index)) |  | ||||||
|  |  | ||||||
|         {:error, %{action: :delete, errors: [ammo_groups: _error], valid?: false} = changeset} -> |  | ||||||
|           ammo_groups_error = changeset |> changeset_errors(:ammo_groups) |> Enum.join(", ") |  | ||||||
|  |  | ||||||
|           prompt = |           prompt = | ||||||
|             dgettext("errors", "Could not delete %{name}: %{error}", |             dgettext("errors", "Could not delete %{name}: %{error}", | ||||||
|               name: changeset |> Changeset.get_field(:name, "container"), |               name: changeset |> Changeset.get_field(:name, "container"), | ||||||
|               error: ammo_groups_error |               error: packs_error | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|           socket |> put_flash(:error, prompt) |           socket |> put_flash(:error, prompt) | ||||||
| @@ -86,33 +82,35 @@ defmodule CanneryWeb.ContainerLive.Show do | |||||||
|     {:noreply, socket |> assign(:view_table, !view_table) |> render_container()} |     {:noreply, socket |> assign(:view_table, !view_table) |> render_container()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :rifle) |> render_container()} |     {:noreply, socket |> assign(:class, :rifle) |> render_container()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "shotgun"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :shotgun) |> render_container()} |     {:noreply, socket |> assign(:class, :shotgun) |> render_container()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "pistol"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :pistol) |> render_container()} |     {:noreply, socket |> assign(:class, :pistol) |> render_container()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => _all}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :all) |> render_container()} |     {:noreply, socket |> assign(:class, :all) |> render_container()} | ||||||
|   end |   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( |   defp render_container( | ||||||
|          %{assigns: %{type: type, live_action: live_action}} = socket, |          %{assigns: %{class: class, live_action: live_action}} = socket, | ||||||
|          id, |          id, | ||||||
|          current_user |          current_user | ||||||
|        ) do |        ) do | ||||||
|     %{name: container_name} = container = Containers.get_container!(id, current_user) |     %{id: container_id, name: container_name} = | ||||||
|     ammo_groups = Ammo.list_ammo_groups_for_container(container, type, current_user) |       container = Containers.get_container!(id, current_user) | ||||||
|     original_counts = ammo_groups |> Ammo.get_original_counts(current_user) |  | ||||||
|     cprs = ammo_groups |> Ammo.get_cprs(current_user) |     packs = Ammo.list_packs(current_user, container_id: container_id, class: class) | ||||||
|     last_used_dates = ammo_groups |> ActivityLog.get_last_used_dates(current_user) |     original_counts = packs |> Ammo.get_original_counts(current_user) | ||||||
|  |     cprs = packs |> Ammo.get_cprs(current_user) | ||||||
|  |     last_used_dates = packs |> ActivityLog.get_last_used_dates(current_user) | ||||||
|  |  | ||||||
|     page_title = |     page_title = | ||||||
|       case live_action do |       case live_action do | ||||||
| @@ -124,9 +122,9 @@ defmodule CanneryWeb.ContainerLive.Show do | |||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       container: container, |       container: container, | ||||||
|       round_count: Ammo.get_round_count_for_container!(container, current_user), |       round_count: Ammo.get_round_count(current_user, container_id: container.id), | ||||||
|       ammo_groups_count: Ammo.get_ammo_groups_count_for_container!(container, current_user), |       packs_count: Ammo.get_packs_count(current_user, container_id: container.id), | ||||||
|       ammo_groups: ammo_groups, |       packs: packs, | ||||||
|       original_counts: original_counts, |       original_counts: original_counts, | ||||||
|       cprs: cprs, |       cprs: cprs, | ||||||
|       last_used_dates: last_used_dates, |       last_used_dates: last_used_dates, | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
|  |  | ||||||
|   <span class="rounded-lg title text-lg"> |   <span class="rounded-lg title text-lg"> | ||||||
|     <%= gettext("Packs:") %> |     <%= gettext("Packs:") %> | ||||||
|     <%= @ammo_groups_count %> |     <%= @packs_count %> | ||||||
|   </span> |   </span> | ||||||
|  |  | ||||||
|   <span class="rounded-lg title text-lg"> |   <span class="rounded-lg title text-lg"> | ||||||
| @@ -30,7 +30,7 @@ | |||||||
|  |  | ||||||
|   <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"> | ||||||
|     <.link |     <.link | ||||||
|       patch={Routes.container_show_path(Endpoint, :edit, @container)} |       patch={~p"/container/edit/#{@container}"} | ||||||
|       class="text-primary-600 link" |       class="text-primary-600 link" | ||||||
|       aria-label={dgettext("actions", "Edit %{container_name}", container_name: @container.name)} |       aria-label={dgettext("actions", "Edit %{container_name}", container_name: @container.name)} | ||||||
|     > |     > | ||||||
| @@ -61,10 +61,7 @@ | |||||||
|         <%= display_emoji("😔") %> |         <%= display_emoji("😔") %> | ||||||
|       </h2> |       </h2> | ||||||
|  |  | ||||||
|       <.link |       <.link patch={~p"/container/edit_tags/#{@container}"} class="btn btn-primary"> | ||||||
|         patch={Routes.container_show_path(Endpoint, :edit_tags, @container)} |  | ||||||
|         class="btn btn-primary" |  | ||||||
|       > |  | ||||||
|         <%= dgettext("actions", "Why not add one?") %> |         <%= dgettext("actions", "Why not add one?") %> | ||||||
|       </.link> |       </.link> | ||||||
|     </div> |     </div> | ||||||
| @@ -73,10 +70,7 @@ | |||||||
|       <.simple_tag_card :for={tag <- @container.tags} tag={tag} /> |       <.simple_tag_card :for={tag <- @container.tags} tag={tag} /> | ||||||
|  |  | ||||||
|       <div class="mx-4 my-2"> |       <div class="mx-4 my-2"> | ||||||
|         <.link |         <.link patch={~p"/container/edit_tags/#{@container}"} class="text-primary-600 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> |           <i class="fa-fw fa-lg fas fa-tags"></i> | ||||||
|         </.link> |         </.link> | ||||||
|       </div> |       </div> | ||||||
| @@ -89,16 +83,16 @@ | |||||||
|     <.form |     <.form | ||||||
|       :let={f} |       :let={f} | ||||||
|       for={%{}} |       for={%{}} | ||||||
|       as={:ammo_type} |       as={:type} | ||||||
|       phx-change="change_type" |       phx-change="change_class" | ||||||
|       phx-submit="change_type" |       phx-submit="change_class" | ||||||
|       class="flex items-center" |       class="flex items-center" | ||||||
|     > |     > | ||||||
|       <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> |       <%= label(f, :class, gettext("Class"), class: "title text-primary-600 text-lg text-center") %> | ||||||
|  |  | ||||||
|       <%= select( |       <%= select( | ||||||
|         f, |         f, | ||||||
|         :type, |         :class, | ||||||
|         [ |         [ | ||||||
|           {gettext("All"), :all}, |           {gettext("All"), :all}, | ||||||
|           {gettext("Rifle"), :rifle}, |           {gettext("Rifle"), :rifle}, | ||||||
| @@ -106,7 +100,7 @@ | |||||||
|           {gettext("Pistol"), :pistol} |           {gettext("Pistol"), :pistol} | ||||||
|         ], |         ], | ||||||
|         class: "mx-2 my-1 min-w-md input input-primary", |         class: "mx-2 my-1 min-w-md input input-primary", | ||||||
|         value: @type |         value: @class | ||||||
|       ) %> |       ) %> | ||||||
|     </.form> |     </.form> | ||||||
|  |  | ||||||
| @@ -118,33 +112,48 @@ | |||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|   <div class="w-full p-4"> |   <div class="w-full p-4"> | ||||||
|     <%= if @ammo_groups |> Enum.empty?() do %> |     <%= if @packs |> Enum.empty?() do %> | ||||||
|       <h2 class="mx-4 title text-lg text-primary-600 text-center"> |       <h2 class="mx-4 title text-lg text-primary-600 text-center"> | ||||||
|         <%= gettext("No ammo in this container") %> |         <%= gettext("No ammo in this container") %> | ||||||
|       </h2> |       </h2> | ||||||
|     <% else %> |     <% else %> | ||||||
|       <%= if @view_table do %> |       <%= if @view_table do %> | ||||||
|         <.live_component |         <.live_component | ||||||
|           module={CanneryWeb.Components.AmmoGroupTableComponent} |           module={CanneryWeb.Components.PackTableComponent} | ||||||
|           id="ammo-type-show-table" |           id="pack-show-table" | ||||||
|           ammo_groups={@ammo_groups} |           packs={@packs} | ||||||
|           current_user={@current_user} |           current_user={@current_user} | ||||||
|           show_used={false} |           show_used={false} | ||||||
|         > |         > | ||||||
|           <:ammo_type :let={%{name: ammo_type_name} = ammo_type}> |           <:type :let={%{name: type_name} = type}> | ||||||
|             <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link"> |             <.link navigate={~p"/type/#{type}"} class="link"> | ||||||
|               <%= ammo_type_name %> |               <%= type_name %> | ||||||
|             </.link> |             </.link> | ||||||
|           </:ammo_type> |           </:type> | ||||||
|  |           <:actions :let={%{count: pack_count} = pack}> | ||||||
|  |             <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center"> | ||||||
|  |               <.link | ||||||
|  |                 navigate={~p"/ammo/show/#{pack}"} | ||||||
|  |                 class="text-primary-600 link" | ||||||
|  |                 aria-label={ | ||||||
|  |                   dgettext("actions", "View pack of %{pack_count} bullets", | ||||||
|  |                     pack_count: pack_count | ||||||
|  |                   ) | ||||||
|  |                 } | ||||||
|  |               > | ||||||
|  |                 <i class="fa-fw fa-lg fas fa-eye"></i> | ||||||
|  |               </.link> | ||||||
|  |             </div> | ||||||
|  |           </:actions> | ||||||
|         </.live_component> |         </.live_component> | ||||||
|       <% else %> |       <% else %> | ||||||
|         <div class="flex flex-wrap justify-center items-stretch"> |         <div class="flex flex-wrap justify-center items-stretch"> | ||||||
|           <.ammo_group_card |           <.pack_card | ||||||
|             :for={%{id: ammo_group_id} = ammo_group <- @ammo_groups} |             :for={%{id: pack_id} = pack <- @packs} | ||||||
|             ammo_group={ammo_group} |             pack={pack} | ||||||
|             original_count={Map.fetch!(@original_counts, ammo_group_id)} |             original_count={Map.fetch!(@original_counts, pack_id)} | ||||||
|             cpr={Map.get(@cprs, ammo_group_id)} |             cpr={Map.get(@cprs, pack_id)} | ||||||
|             last_used_date={Map.get(@last_used_dates, ammo_group_id)} |             last_used_date={Map.get(@last_used_dates, pack_id)} | ||||||
|             current_user={@current_user} |             current_user={@current_user} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -155,27 +164,27 @@ | |||||||
|  |  | ||||||
| <%= case @live_action do %> | <%= case @live_action do %> | ||||||
|   <% :edit -> %> |   <% :edit -> %> | ||||||
|     <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}> |     <.modal return_to={~p"/container/#{@container}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.ContainerLive.FormComponent} |         module={CanneryWeb.ContainerLive.FormComponent} | ||||||
|         id={@container.id} |         id={@container.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         container={@container} |         container={@container} | ||||||
|         return_to={Routes.container_show_path(Endpoint, :show, @container)} |         return_to={~p"/container/#{@container}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :edit_tags -> %> |   <% :edit_tags -> %> | ||||||
|     <.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}> |     <.modal return_to={~p"/container/#{@container}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.ContainerLive.EditTagsComponent} |         module={CanneryWeb.ContainerLive.EditTagsComponent} | ||||||
|         id={@container.id} |         id={@container.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         container={@container} |         container={@container} | ||||||
|         return_to={Routes.container_show_path(Endpoint, :show, @container)} |         return_to={~p"/container/#{@container}"} | ||||||
|         current_path={Routes.container_show_path(Endpoint, :edit_tags, @container)} |         current_path={~p"/container/edit_tags/#{@container}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ 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] |   @version Mix.Project.config()[:version] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl"> | <div class="mx-auto px-8 sm:px-16 flex flex-col justify-center items-center text-center space-y-4 max-w-3xl"> | ||||||
|   <img |   <img | ||||||
|     src={Routes.static_path(Endpoint, "/images/cannery.svg")} |     src={~p"/images/cannery.svg"} | ||||||
|     alt={gettext("Cannery logo")} |     alt={gettext("Cannery logo")} | ||||||
|     class="inline-block w-32 hover:-mt-2 hover:mb-2 transition-all duration-500 ease-in-out" |     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")} |     title={gettext("isn't he cute >:3")} | ||||||
| @@ -59,7 +59,7 @@ | |||||||
|       </b> |       </b> | ||||||
|       <p> |       <p> | ||||||
|         <%= if @admins |> Enum.empty?() do %> |         <%= if @admins |> Enum.empty?() do %> | ||||||
|           <.link href={Routes.user_registration_path(Endpoint, :new)} class="hover:underline"> |           <.link href={~p"/users/register"} class="hover:underline"> | ||||||
|             <%= dgettext("prompts", "Register to setup Cannery") %> |             <%= dgettext("prompts", "Register to setup Cannery") %> | ||||||
|           </.link> |           </.link> | ||||||
|         <% else %> |         <% else %> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|     phx-submit="save" |     phx-submit="save" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       :if={@changeset.action && not @changeset.valid?()} |       :if={@changeset.action && not @changeset.valid?} | ||||||
|       class="invalid-feedback col-span-3 text-center" |       class="invalid-feedback col-span-3 text-center" | ||||||
|     > |     > | ||||||
|       <%= changeset_errors(@changeset) %> |       <%= changeset_errors(@changeset) %> | ||||||
|   | |||||||
| @@ -9,11 +9,11 @@ | |||||||
|       <%= display_emoji("😔") %> |       <%= display_emoji("😔") %> | ||||||
|     </h1> |     </h1> | ||||||
|  |  | ||||||
|     <.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary"> |     <.link patch={~p"/invites/new"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Invite someone new!") %> |       <%= dgettext("actions", "Invite someone new!") %> | ||||||
|     </.link> |     </.link> | ||||||
|   <% else %> |   <% else %> | ||||||
|     <.link patch={Routes.invite_index_path(Endpoint, :new)} class="btn btn-primary"> |     <.link patch={~p"/invites/new"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Create Invite") %> |       <%= dgettext("actions", "Create Invite") %> | ||||||
|     </.link> |     </.link> | ||||||
|   <% end %> |   <% end %> | ||||||
| @@ -40,7 +40,7 @@ | |||||||
|         </form> |         </form> | ||||||
|       </:code_actions> |       </:code_actions> | ||||||
|       <.link |       <.link | ||||||
|         patch={Routes.invite_index_path(Endpoint, :edit, invite)} |         patch={~p"/invites/edit/#{invite}"} | ||||||
|         class="text-primary-600 link" |         class="text-primary-600 link" | ||||||
|         aria-label={ |         aria-label={ | ||||||
|           dgettext("actions", "Edit invite for %{invite_name}", invite_name: invite.name) |           dgettext("actions", "Edit invite for %{invite_name}", invite_name: invite.name) | ||||||
| @@ -149,14 +149,14 @@ | |||||||
|   <% end %> |   <% end %> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <.modal :if={@live_action in [:new, :edit]} return_to={Routes.invite_index_path(Endpoint, :index)}> | <.modal :if={@live_action in [:new, :edit]} return_to={~p"/invites"}> | ||||||
|   <.live_component |   <.live_component | ||||||
|     module={CanneryWeb.InviteLive.FormComponent} |     module={CanneryWeb.InviteLive.FormComponent} | ||||||
|     id={@invite.id || :new} |     id={@invite.id || :new} | ||||||
|     title={@page_title} |     title={@page_title} | ||||||
|     action={@live_action} |     action={@live_action} | ||||||
|     invite={@invite} |     invite={@invite} | ||||||
|     return_to={Routes.invite_index_path(Endpoint, :index)} |     return_to={~p"/invites"} | ||||||
|     current_user={@current_user} |     current_user={@current_user} | ||||||
|   /> |   /> | ||||||
| </.modal> | </.modal> | ||||||
|   | |||||||
| @@ -1,38 +1,38 @@ | |||||||
| defmodule CanneryWeb.AmmoGroupLive.FormComponent do | defmodule CanneryWeb.PackLive.FormComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Livecomponent that can update or create an Cannery.Ammo.AmmoGroup |   Livecomponent that can update or create an Cannery.Ammo.Pack | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.Ammo.{AmmoGroup, AmmoType} |   alias Cannery.Ammo.{Pack, Type} | ||||||
|   alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container} |   alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container} | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|   alias Phoenix.LiveView.Socket |   alias Phoenix.LiveView.Socket | ||||||
| 
 | 
 | ||||||
|   @ammo_group_create_limit 10_000 |   @pack_create_limit 10_000 | ||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
|   @spec update( |   @spec update( | ||||||
|           %{:ammo_group => AmmoGroup.t(), :current_user => User.t(), optional(any) => any}, |           %{:pack => Pack.t(), :current_user => User.t(), optional(any) => any}, | ||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update(%{ammo_group: _ammo_group} = assigns, socket) do |   def update(%{pack: _pack} = assigns, socket) do | ||||||
|     socket |> assign(assigns) |> update() |     socket |> assign(assigns) |> update() | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec update(Socket.t()) :: {:ok, Socket.t()} |   @spec update(Socket.t()) :: {:ok, Socket.t()} | ||||||
|   def update(%{assigns: %{current_user: current_user}} = socket) do |   def update(%{assigns: %{current_user: current_user}} = socket) do | ||||||
|     %{assigns: %{ammo_types: ammo_types, containers: containers}} = |     %{assigns: %{types: types, containers: containers}} = | ||||||
|       socket = |       socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(:ammo_group_create_limit, @ammo_group_create_limit) |       |> assign(:pack_create_limit, @pack_create_limit) | ||||||
|       |> assign(:ammo_types, Ammo.list_ammo_types(current_user, :all)) |       |> assign(:types, Ammo.list_types(current_user)) | ||||||
|       |> assign_new(:containers, fn -> Containers.list_containers(current_user) end) |       |> assign_new(:containers, fn -> Containers.list_containers(current_user) end) | ||||||
| 
 | 
 | ||||||
|     params = |     params = | ||||||
|       if ammo_types |> List.first() |> is_nil(), |       if types |> List.first() |> is_nil(), | ||||||
|         do: %{}, |         do: %{}, | ||||||
|         else: %{} |> Map.put("ammo_type_id", ammo_types |> List.first() |> Map.get(:id)) |         else: %{} |> Map.put("type_id", types |> List.first() |> Map.get(:id)) | ||||||
| 
 | 
 | ||||||
|     params = |     params = | ||||||
|       if containers |> List.first() |> is_nil(), |       if containers |> List.first() |> is_nil(), | ||||||
| @@ -43,16 +43,16 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
|   def handle_event("validate", %{"ammo_group" => ammo_group_params}, socket) do |   def handle_event("validate", %{"pack" => pack_params}, socket) do | ||||||
|     {:noreply, socket |> assign_changeset(ammo_group_params, :validate)} |     {:noreply, socket |> assign_changeset(pack_params, :validate)} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "save", |         "save", | ||||||
|         %{"ammo_group" => ammo_group_params}, |         %{"pack" => pack_params}, | ||||||
|         %{assigns: %{action: action}} = socket |         %{assigns: %{action: action}} = socket | ||||||
|       ) do |       ) do | ||||||
|     save_ammo_group(socket, action, ammo_group_params) |     save_pack(socket, action, pack_params) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # HTML Helpers |   # HTML Helpers | ||||||
| @@ -62,16 +62,16 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
|     containers |> Enum.map(fn %{id: id, name: name} -> {name, id} end) |     containers |> Enum.map(fn %{id: id, name: name} -> {name, id} end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec ammo_type_options([AmmoType.t()]) :: [{String.t(), AmmoType.id()}] |   @spec type_options([Type.t()]) :: [{String.t(), Type.id()}] | ||||||
|   defp ammo_type_options(ammo_types) do |   defp type_options(types) do | ||||||
|     ammo_types |> Enum.map(fn %{id: id, name: name} -> {name, id} end) |     types |> Enum.map(fn %{id: id, name: name} -> {name, id} end) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # Save Helpers |   # Save Helpers | ||||||
| 
 | 
 | ||||||
|   defp assign_changeset( |   defp assign_changeset( | ||||||
|          %{assigns: %{action: action, ammo_group: ammo_group, current_user: user}} = socket, |          %{assigns: %{action: action, pack: pack, current_user: user}} = socket, | ||||||
|          ammo_group_params, |          pack_params, | ||||||
|          changeset_action \\ nil |          changeset_action \\ nil | ||||||
|        ) do |        ) do | ||||||
|     default_action = |     default_action = | ||||||
| @@ -83,12 +83,12 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
|     changeset = |     changeset = | ||||||
|       case default_action do |       case default_action do | ||||||
|         :insert -> |         :insert -> | ||||||
|           ammo_type = maybe_get_ammo_type(ammo_group_params, user) |           type = maybe_get_type(pack_params, user) | ||||||
|           container = maybe_get_container(ammo_group_params, user) |           container = maybe_get_container(pack_params, user) | ||||||
|           ammo_group |> AmmoGroup.create_changeset(ammo_type, container, user, ammo_group_params) |           pack |> Pack.create_changeset(type, container, user, pack_params) | ||||||
| 
 | 
 | ||||||
|         :update -> |         :update -> | ||||||
|           ammo_group |> AmmoGroup.update_changeset(ammo_group_params, user) |           pack |> Pack.update_changeset(pack_params, user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|     changeset = |     changeset = | ||||||
| @@ -107,22 +107,21 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
| 
 | 
 | ||||||
|   defp maybe_get_container(_params_not_found, _user), do: nil |   defp maybe_get_container(_params_not_found, _user), do: nil | ||||||
| 
 | 
 | ||||||
|   defp maybe_get_ammo_type(%{"ammo_type_id" => ammo_type_id}, user) |   defp maybe_get_type(%{"type_id" => type_id}, user) | ||||||
|        when is_binary(ammo_type_id) do |        when is_binary(type_id) do | ||||||
|     ammo_type_id |> Ammo.get_ammo_type!(user) |     type_id |> Ammo.get_type!(user) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp maybe_get_ammo_type(_params_not_found, _user), do: nil |   defp maybe_get_type(_params_not_found, _user), do: nil | ||||||
| 
 | 
 | ||||||
|   defp save_ammo_group( |   defp save_pack( | ||||||
|          %{assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to}} = |          %{assigns: %{pack: pack, current_user: current_user, return_to: return_to}} = socket, | ||||||
|            socket, |  | ||||||
|          :edit, |          :edit, | ||||||
|          ammo_group_params |          pack_params | ||||||
|        ) do |        ) do | ||||||
|     socket = |     socket = | ||||||
|       case Ammo.update_ammo_group(ammo_group, ammo_group_params, current_user) do |       case Ammo.update_pack(pack, pack_params, current_user) do | ||||||
|         {:ok, _ammo_group} -> |         {:ok, _pack} -> | ||||||
|           prompt = dgettext("prompts", "Ammo updated successfully") |           prompt = dgettext("prompts", "Ammo updated successfully") | ||||||
|           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) |           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) | ||||||
| 
 | 
 | ||||||
| @@ -133,24 +132,24 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
|     {:noreply, socket} |     {:noreply, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp save_ammo_group( |   defp save_pack( | ||||||
|          %{assigns: %{changeset: changeset}} = socket, |          %{assigns: %{changeset: changeset}} = socket, | ||||||
|          action, |          action, | ||||||
|          %{"multiplier" => multiplier_str} = ammo_group_params |          %{"multiplier" => multiplier_str} = pack_params | ||||||
|        ) |        ) | ||||||
|        when action in [:new, :clone] do |        when action in [:new, :clone] do | ||||||
|     socket = |     socket = | ||||||
|       case multiplier_str |> Integer.parse() do |       case multiplier_str |> Integer.parse() do | ||||||
|         {multiplier, _remainder} |         {multiplier, _remainder} | ||||||
|         when multiplier >= 1 and multiplier <= @ammo_group_create_limit -> |         when multiplier >= 1 and multiplier <= @pack_create_limit -> | ||||||
|           socket |> create_multiple(ammo_group_params, multiplier) |           socket |> create_multiple(pack_params, multiplier) | ||||||
| 
 | 
 | ||||||
|         {multiplier, _remainder} -> |         {multiplier, _remainder} -> | ||||||
|           error_msg = |           error_msg = | ||||||
|             dgettext( |             dgettext( | ||||||
|               "errors", |               "errors", | ||||||
|               "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}", |               "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}", | ||||||
|               max: @ammo_group_create_limit, |               max: @pack_create_limit, | ||||||
|               multiplier: multiplier |               multiplier: multiplier | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
| @@ -176,11 +175,11 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do | |||||||
| 
 | 
 | ||||||
|   defp create_multiple( |   defp create_multiple( | ||||||
|          %{assigns: %{current_user: current_user, return_to: return_to}} = socket, |          %{assigns: %{current_user: current_user, return_to: return_to}} = socket, | ||||||
|          ammo_group_params, |          pack_params, | ||||||
|          multiplier |          multiplier | ||||||
|        ) do |        ) do | ||||||
|     case Ammo.create_ammo_groups(ammo_group_params, multiplier, current_user) do |     case Ammo.create_packs(pack_params, multiplier, current_user) do | ||||||
|       {:ok, {count, _ammo_groups}} -> |       {:ok, {count, _packs}} -> | ||||||
|         prompt = |         prompt = | ||||||
|           dngettext( |           dngettext( | ||||||
|             "prompts", |             "prompts", | ||||||
| @@ -6,24 +6,24 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@changeset} |     for={@changeset} | ||||||
|     id="ammo_group-form" |     id="pack-form" | ||||||
|     phx-target={@myself} |     phx-target={@myself} | ||||||
|     phx-change="validate" |     phx-change="validate" | ||||||
|     phx-submit="save" |     phx-submit="save" | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       :if={@changeset.action && not @changeset.valid?()} |       :if={@changeset.action && not @changeset.valid?} | ||||||
|       class="invalid-feedback col-span-3 text-center" |       class="invalid-feedback col-span-3 text-center" | ||||||
|     > |     > | ||||||
|       <%= changeset_errors(@changeset) %> |       <%= changeset_errors(@changeset) %> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <%= label(f, :ammo_type_id, gettext("Ammo type"), class: "title text-lg text-primary-600") %> |     <%= label(f, :type_id, gettext("Type"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= select(f, :ammo_type_id, ammo_type_options(@ammo_types), |     <%= select(f, :type_id, type_options(@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, :type_id, "col-span-3 text-center") %> | ||||||
| 
 | 
 | ||||||
|     <%= label(f, :count, gettext("Count"), class: "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, | ||||||
| @@ -39,6 +39,13 @@ | |||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :price_paid, "col-span-3 text-center") %> |     <%= error_tag(f, :price_paid, "col-span-3 text-center") %> | ||||||
| 
 | 
 | ||||||
|  |     <%= label(f, :lot_number, gettext("Lot number"), class: "title text-lg text-primary-600") %> | ||||||
|  |     <%= text_input(f, :lot_number, | ||||||
|  |       class: "text-center col-span-2 input input-primary", | ||||||
|  |       maxlength: 255 | ||||||
|  |     ) %> | ||||||
|  |     <%= error_tag(f, :price_paid, "col-span-3 text-center") %> | ||||||
|  | 
 | ||||||
|     <%= label(f, :purchased_on, gettext("Purchased on"), class: "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, |     <%= date_input(f, :purchased_on, | ||||||
|       class: "input input-primary col-span-2", |       class: "input input-primary col-span-2", | ||||||
| @@ -49,9 +56,8 @@ | |||||||
| 
 | 
 | ||||||
|     <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %> |     <%= label(f, :notes, gettext("Notes"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= textarea(f, :notes, |     <%= textarea(f, :notes, | ||||||
|       id: "ammo-group-form-notes", |       id: "pack-form-notes", | ||||||
|       class: "text-center col-span-2 input input-primary", |       class: "text-center col-span-2 input input-primary", | ||||||
|       phx_hook: "MaintainAttrs", |  | ||||||
|       phx_update: "ignore" |       phx_update: "ignore" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :notes, "col-span-3 text-center") %> |     <%= error_tag(f, :notes, "col-span-3 text-center") %> | ||||||
| @@ -68,7 +74,7 @@ | |||||||
| 
 | 
 | ||||||
|         <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %> |         <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %> | ||||||
|         <%= number_input(f, :multiplier, |         <%= number_input(f, :multiplier, | ||||||
|           max: @ammo_group_create_limit, |           max: @pack_create_limit, | ||||||
|           class: "text-center input input-primary", |           class: "text-center input input-primary", | ||||||
|           value: 1, |           value: 1, | ||||||
|           phx_update: "ignore" |           phx_update: "ignore" | ||||||
| @@ -1,34 +1,39 @@ | |||||||
| defmodule CanneryWeb.AmmoGroupLive.Index do | defmodule CanneryWeb.PackLive.Index do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Liveview to show a Cannery.Ammo.AmmoGroup index |   Liveview to show a Cannery.Ammo.Pack index | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use CanneryWeb, :live_view |   use CanneryWeb, :live_view | ||||||
|   alias Cannery.{Ammo, Ammo.AmmoGroup, Containers} |   alias Cannery.{Ammo, Ammo.Pack, Containers} | ||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
|   def mount(%{"search" => search}, _session, socket) do |   def mount(%{"search" => search}, _session, socket) do | ||||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: search) |> display_ammo_groups()} |     socket = | ||||||
|  |       socket | ||||||
|  |       |> assign(class: :all, show_used: false, search: search) | ||||||
|  |       |> display_packs() | ||||||
|  | 
 | ||||||
|  |     {:ok, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def mount(_params, _session, socket) do |   def mount(_params, _session, socket) do | ||||||
|     {:ok, socket |> assign(type: :all, show_used: false, search: nil) |> display_ammo_groups()} |     {:ok, socket |> assign(class: :all, show_used: false, search: nil) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
|   def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do |   def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do | ||||||
|     {:noreply, apply_action(socket, live_action, params) |> display_ammo_groups()} |     {:noreply, apply_action(socket, live_action, params) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp apply_action( |   defp apply_action( | ||||||
|          %{assigns: %{current_user: current_user}} = socket, |          %{assigns: %{current_user: current_user}} = socket, | ||||||
|          :add_shot_group, |          :add_shot_record, | ||||||
|          %{"id" => id} |          %{"id" => id} | ||||||
|        ) do |        ) do | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Record shots"), |       page_title: gettext("Record shots"), | ||||||
|       ammo_group: Ammo.get_ammo_group!(id, current_user) |       pack: Ammo.get_pack!(id, current_user) | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -36,7 +41,7 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Move ammo"), |       page_title: gettext("Move ammo"), | ||||||
|       ammo_group: Ammo.get_ammo_group!(id, current_user) |       pack: Ammo.get_pack!(id, current_user) | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -44,7 +49,7 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Edit ammo"), |       page_title: gettext("Edit ammo"), | ||||||
|       ammo_group: Ammo.get_ammo_group!(id, current_user) |       pack: Ammo.get_pack!(id, current_user) | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -52,7 +57,7 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: dgettext("actions", "Add Ammo"), |       page_title: dgettext("actions", "Add Ammo"), | ||||||
|       ammo_group: %{Ammo.get_ammo_group!(id, current_user) | id: nil} |       pack: %{Ammo.get_pack!(id, current_user) | id: nil} | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -60,7 +65,7 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: dgettext("actions", "Add Ammo"), |       page_title: dgettext("actions", "Add Ammo"), | ||||||
|       ammo_group: %AmmoGroup{} |       pack: %Pack{} | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -69,7 +74,7 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Ammo"), |       page_title: gettext("Ammo"), | ||||||
|       search: nil, |       search: nil, | ||||||
|       ammo_group: nil |       pack: nil | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @@ -78,86 +83,82 @@ defmodule CanneryWeb.AmmoGroupLive.Index do | |||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Ammo"), |       page_title: gettext("Ammo"), | ||||||
|       search: search, |       search: search, | ||||||
|       ammo_group: nil |       pack: 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_pack!(id, current_user) |> Ammo.delete_pack!(current_user) | ||||||
| 
 | 
 | ||||||
|     prompt = dgettext("prompts", "Ammo deleted succesfully") |     prompt = dgettext("prompts", "Ammo deleted succesfully") | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_groups()} |     {:noreply, socket |> put_flash(:info, prompt) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "toggle_staged", |         "toggle_staged", | ||||||
|         %{"ammo_group_id" => id}, |         %{"pack_id" => id}, | ||||||
|         %{assigns: %{current_user: current_user}} = socket |         %{assigns: %{current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     ammo_group = Ammo.get_ammo_group!(id, current_user) |     pack = Ammo.get_pack!(id, current_user) | ||||||
| 
 | 
 | ||||||
|     {:ok, _ammo_group} = |     {:ok, _pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user) | ||||||
|       ammo_group |> Ammo.update_ammo_group(%{"staged" => !ammo_group.staged}, current_user) |  | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket |> display_ammo_groups()} |     {:noreply, socket |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do |   def handle_event("toggle_show_used", _params, %{assigns: %{show_used: show_used}} = socket) do | ||||||
|     {:noreply, socket |> assign(:show_used, !show_used) |> display_ammo_groups()} |     {:noreply, socket |> assign(:show_used, !show_used) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do | ||||||
|     {:noreply, socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :index))} |     {:noreply, socket |> push_patch(to: ~p"/ammo")} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do | ||||||
|     socket = |     {:noreply, socket |> push_patch(to: ~p"/ammo/search/#{search_term}")} | ||||||
|       socket |> push_patch(to: Routes.ammo_group_index_path(Endpoint, :search, search_term)) |  | ||||||
| 
 |  | ||||||
|     {:noreply, socket} |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :rifle) |> display_ammo_groups()} |     {:noreply, socket |> assign(:class, :rifle) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "shotgun"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :shotgun) |> display_ammo_groups()} |     {:noreply, socket |> assign(:class, :shotgun) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "pistol"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :pistol) |> display_ammo_groups()} |     {:noreply, socket |> assign(:class, :pistol) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => _all}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :all) |> display_ammo_groups()} |     {:noreply, socket |> assign(:class, :all) |> display_packs()} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp display_ammo_groups( |   defp display_packs( | ||||||
|          %{ |          %{ | ||||||
|            assigns: %{ |            assigns: %{ | ||||||
|              type: type, |              class: class, | ||||||
|              search: search, |              search: search, | ||||||
|              current_user: current_user, |              current_user: current_user, | ||||||
|              show_used: show_used |              show_used: show_used | ||||||
|            } |            } | ||||||
|          } = socket |          } = socket | ||||||
|        ) do |        ) do | ||||||
|     # get total number of ammo groups to determine whether to display onboarding |     # get total number of packs to determine whether to display onboarding | ||||||
|     # prompts |     # prompts | ||||||
|     ammo_groups_count = Ammo.get_ammo_groups_count!(current_user, true) |     packs_count = Ammo.get_packs_count(current_user, show_used: true) | ||||||
|     ammo_groups = Ammo.list_ammo_groups(search, type, current_user, show_used) |     packs = Ammo.list_packs(current_user, search: search, class: class, show_used: show_used) | ||||||
|     ammo_types_count = Ammo.get_ammo_types_count!(current_user) |     types_count = Ammo.get_types_count!(current_user) | ||||||
|     containers_count = Containers.get_containers_count!(current_user) |     containers_count = Containers.get_containers_count!(current_user) | ||||||
| 
 | 
 | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       ammo_groups: ammo_groups, |       packs: packs, | ||||||
|       ammo_types_count: ammo_types_count, |       types_count: types_count, | ||||||
|       containers_count: containers_count, |       containers_count: containers_count, | ||||||
|       ammo_groups_count: ammo_groups_count |       packs_count: packs_count | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @@ -10,31 +10,31 @@ | |||||||
|           <%= dgettext("prompts", "You'll need to") %> |           <%= dgettext("prompts", "You'll need to") %> | ||||||
|         </h2> |         </h2> | ||||||
| 
 | 
 | ||||||
|         <.link navigate={Routes.container_index_path(Endpoint, :new)} class="btn btn-primary"> |         <.link navigate={~p"/containers/new"} class="btn btn-primary"> | ||||||
|           <%= dgettext("actions", "add a container first") %> |           <%= dgettext("actions", "add a container first") %> | ||||||
|         </.link> |         </.link> | ||||||
|       </div> |       </div> | ||||||
|     <% @ammo_types_count == 0 -> %> |     <% @types_count == 0 -> %> | ||||||
|       <div class="flex justify-center items-center"> |       <div class="flex justify-center items-center"> | ||||||
|         <h2 class="m-2 title text-md text-primary-600"> |         <h2 class="m-2 title text-md text-primary-600"> | ||||||
|           <%= dgettext("prompts", "You'll need to") %> |           <%= dgettext("prompts", "You'll need to") %> | ||||||
|         </h2> |         </h2> | ||||||
| 
 | 
 | ||||||
|         <.link navigate={Routes.ammo_type_index_path(Endpoint, :new)} class="btn btn-primary"> |         <.link navigate={~p"/catalog/new"} class="btn btn-primary"> | ||||||
|           <%= dgettext("actions", "add an ammo type first") %> |           <%= dgettext("actions", "add a type first") %> | ||||||
|         </.link> |         </.link> | ||||||
|       </div> |       </div> | ||||||
|     <% @ammo_groups_count == 0 -> %> |     <% @packs_count == 0 -> %> | ||||||
|       <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> | ||||||
| 
 | 
 | ||||||
|       <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary"> |       <.link patch={~p"/ammo/new"} class="btn btn-primary"> | ||||||
|         <%= dgettext("actions", "Add your first box!") %> |         <%= dgettext("actions", "Add your first box!") %> | ||||||
|       </.link> |       </.link> | ||||||
|     <% true -> %> |     <% true -> %> | ||||||
|       <.link patch={Routes.ammo_group_index_path(Endpoint, :new)} class="btn btn-primary"> |       <.link patch={~p"/ammo/new"} class="btn btn-primary"> | ||||||
|         <%= dgettext("actions", "Add Ammo") %> |         <%= dgettext("actions", "Add Ammo") %> | ||||||
|       </.link> |       </.link> | ||||||
| 
 | 
 | ||||||
| @@ -42,16 +42,18 @@ | |||||||
|         <.form |         <.form | ||||||
|           :let={f} |           :let={f} | ||||||
|           for={%{}} |           for={%{}} | ||||||
|           as={:ammo_type} |           as={:type} | ||||||
|           phx-change="change_type" |           phx-change="change_class" | ||||||
|           phx-submit="change_type" |           phx-submit="change_class" | ||||||
|           class="flex items-center" |           class="flex items-center" | ||||||
|         > |         > | ||||||
|           <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> |           <%= label(f, :class, gettext("Class"), | ||||||
|  |             class: "title text-primary-600 text-lg text-center" | ||||||
|  |           ) %> | ||||||
| 
 | 
 | ||||||
|           <%= select( |           <%= select( | ||||||
|             f, |             f, | ||||||
|             :type, |             :class, | ||||||
|             [ |             [ | ||||||
|               {gettext("All"), :all}, |               {gettext("All"), :all}, | ||||||
|               {gettext("Rifle"), :rifle}, |               {gettext("Rifle"), :rifle}, | ||||||
| @@ -59,7 +61,7 @@ | |||||||
|               {gettext("Pistol"), :pistol} |               {gettext("Pistol"), :pistol} | ||||||
|             ], |             ], | ||||||
|             class: "mx-2 my-1 min-w-md input input-primary", |             class: "mx-2 my-1 min-w-md input input-primary", | ||||||
|             value: @type |             value: @class | ||||||
|           ) %> |           ) %> | ||||||
|         </.form> |         </.form> | ||||||
| 
 | 
 | ||||||
| @@ -87,70 +89,64 @@ | |||||||
|         </.toggle_button> |         </.toggle_button> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <%= if @ammo_groups |> Enum.empty?() do %> |       <%= if @packs |> 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> | ||||||
|       <% else %> |       <% else %> | ||||||
|         <.live_component |         <.live_component | ||||||
|           module={CanneryWeb.Components.AmmoGroupTableComponent} |           module={CanneryWeb.Components.PackTableComponent} | ||||||
|           id="ammo-group-index-table" |           id="pack-index-table" | ||||||
|           ammo_groups={@ammo_groups} |           packs={@packs} | ||||||
|           current_user={@current_user} |           current_user={@current_user} | ||||||
|           show_used={@show_used} |           show_used={@show_used} | ||||||
|         > |         > | ||||||
|           <:ammo_type :let={%{name: ammo_type_name} = ammo_type}> |           <:type :let={%{name: type_name} = type}> | ||||||
|             <.link navigate={Routes.ammo_type_show_path(Endpoint, :show, ammo_type)} class="link"> |             <.link navigate={~p"/type/#{type}"} class="link"> | ||||||
|               <%= ammo_type_name %> |               <%= type_name %> | ||||||
|             </.link> |             </.link> | ||||||
|           </:ammo_type> |           </:type> | ||||||
|           <:range :let={ammo_group}> |           <:range :let={pack}> | ||||||
|             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> |             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||||
|               <button |               <button | ||||||
|                 type="button" |                 type="button" | ||||||
|                 class="mx-2 my-1 text-sm 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-pack_id={pack.id} | ||||||
|               > |               > | ||||||
|                 <%= if ammo_group.staged, |                 <%= if pack.staged, | ||||||
|                   do: dgettext("actions", "Unstage"), |                   do: dgettext("actions", "Unstage"), | ||||||
|                   else: dgettext("actions", "Stage") %> |                   else: dgettext("actions", "Stage") %> | ||||||
|               </button> |               </button> | ||||||
| 
 | 
 | ||||||
|               <.link |               <.link | ||||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :add_shot_group, ammo_group)} |                 patch={~p"/ammo/add_shot_record/#{pack}"} | ||||||
|                 class="mx-2 my-1 text-sm btn btn-primary" |                 class="mx-2 my-1 text-sm btn btn-primary" | ||||||
|               > |               > | ||||||
|                 <%= dgettext("actions", "Record shots") %> |                 <%= dgettext("actions", "Record shots") %> | ||||||
|               </.link> |               </.link> | ||||||
|             </div> |             </div> | ||||||
|           </:range> |           </:range> | ||||||
|           <:container :let={{ammo_group, %{name: container_name} = container}}> |           <:container :let={{pack, %{name: container_name} = container}}> | ||||||
|             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> |             <div class="min-w-20 py-2 px-4 h-full flex flew-wrap justify-center items-center"> | ||||||
|               <.link |               <.link navigate={~p"/container/#{container}"} class="mx-2 my-1 link"> | ||||||
|                 navigate={Routes.container_show_path(Endpoint, :show, container)} |  | ||||||
|                 class="mx-2 my-1 link" |  | ||||||
|               > |  | ||||||
|                 <%= container_name %> |                 <%= container_name %> | ||||||
|               </.link> |               </.link> | ||||||
| 
 | 
 | ||||||
|               <.link |               <.link patch={~p"/ammo/move/#{pack}"} class="mx-2 my-1 text-sm btn btn-primary"> | ||||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :move, ammo_group)} |  | ||||||
|                 class="mx-2 my-1 text-sm btn btn-primary" |  | ||||||
|               > |  | ||||||
|                 <%= dgettext("actions", "Move ammo") %> |                 <%= dgettext("actions", "Move ammo") %> | ||||||
|               </.link> |               </.link> | ||||||
|             </div> |             </div> | ||||||
|           </:container> |           </:container> | ||||||
|           <:actions :let={%{count: ammo_group_count} = ammo_group}> |           <:actions :let={%{count: pack_count} = pack}> | ||||||
|             <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center"> |             <div class="py-2 px-4 h-full space-x-4 flex justify-center items-center"> | ||||||
|               <.link |               <.link | ||||||
|                 navigate={Routes.ammo_group_show_path(Endpoint, :show, ammo_group)} |                 navigate={~p"/ammo/show/#{pack}"} | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "View ammo group of %{ammo_group_count} bullets", |                   dgettext("actions", "View pack of %{pack_count} bullets", | ||||||
|                     ammo_group_count: ammo_group_count |                     pack_count: pack_count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -158,11 +154,11 @@ | |||||||
|               </.link> |               </.link> | ||||||
| 
 | 
 | ||||||
|               <.link |               <.link | ||||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :edit, ammo_group)} |                 patch={~p"/ammo/edit/#{pack}"} | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Edit ammo group of %{ammo_group_count} bullets", |                   dgettext("actions", "Edit pack of %{pack_count} bullets", | ||||||
|                     ammo_group_count: ammo_group_count |                     pack_count: pack_count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -170,11 +166,11 @@ | |||||||
|               </.link> |               </.link> | ||||||
| 
 | 
 | ||||||
|               <.link |               <.link | ||||||
|                 patch={Routes.ammo_group_index_path(Endpoint, :clone, ammo_group)} |                 patch={~p"/ammo/clone/#{pack}"} | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Clone ammo group of %{ammo_group_count} bullets", |                   dgettext("actions", "Clone pack of %{pack_count} bullets", | ||||||
|                     ammo_group_count: ammo_group_count |                     pack_count: pack_count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -185,11 +181,11 @@ | |||||||
|                 href="#" |                 href="#" | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 phx-click="delete" |                 phx-click="delete" | ||||||
|                 phx-value-id={ammo_group.id} |                 phx-value-id={pack.id} | ||||||
|                 data-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?")} | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets", |                   dgettext("actions", "Delete pack of %{pack_count} bullets", | ||||||
|                     ammo_group_count: ammo_group_count |                     pack_count: pack_count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -204,38 +200,38 @@ | |||||||
| 
 | 
 | ||||||
| <%= case @live_action do %> | <%= case @live_action do %> | ||||||
|   <% create when create in [:new, :edit, :clone] -> %> |   <% create when create in [:new, :edit, :clone] -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/ammo"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.AmmoGroupLive.FormComponent} |         module={CanneryWeb.PackLive.FormComponent} | ||||||
|         id={@ammo_group.id || :new} |         id={@pack.id || :new} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_index_path(Endpoint, :index)} |         return_to={~p"/ammo"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :add_shot_group -> %> |   <% :add_shot_record -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/ammo"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.AddShotGroupComponent} |         module={CanneryWeb.Components.AddShotRecordComponent} | ||||||
|         id={:new} |         id={:new} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_index_path(Endpoint, :index)} |         return_to={~p"/ammo"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :move -> %> |   <% :move -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/ammo"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.MoveAmmoGroupComponent} |         module={CanneryWeb.Components.MovePackComponent} | ||||||
|         id={@ammo_group.id} |         id={@pack.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_index_path(Endpoint, :index)} |         return_to={~p"/ammo"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
| @@ -1,13 +1,12 @@ | |||||||
| defmodule CanneryWeb.AmmoGroupLive.Show do | defmodule CanneryWeb.PackLive.Show do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Liveview for showing and editing an Cannery.Ammo.AmmoGroup |   Liveview for showing and editing an Cannery.Ammo.Pack | ||||||
|   """ |   """ | ||||||
| 
 | 
 | ||||||
|   use CanneryWeb, :live_view |   use CanneryWeb, :live_view | ||||||
|   alias Cannery.{ActivityLog, ActivityLog.ShotGroup} |   alias Cannery.{ActivityLog, ActivityLog.ShotRecord} | ||||||
|   alias Cannery.{Ammo, Ammo.AmmoGroup} |   alias Cannery.{Ammo, Ammo.Pack} | ||||||
|   alias Cannery.{ComparableDate, Containers} |   alias Cannery.{ComparableDate, Containers} | ||||||
|   alias CanneryWeb.Endpoint |  | ||||||
|   alias Phoenix.LiveView.Socket |   alias Phoenix.LiveView.Socket | ||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
| @@ -15,16 +14,16 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
| 
 | 
 | ||||||
|   @impl true |   @impl true | ||||||
|   def handle_params( |   def handle_params( | ||||||
|         %{"id" => id, "shot_group_id" => shot_group_id}, |         %{"id" => id, "shot_record_id" => shot_record_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 | ||||||
|     shot_group = ActivityLog.get_shot_group!(shot_group_id, current_user) |     shot_record = ActivityLog.get_shot_record!(shot_record_id, current_user) | ||||||
| 
 | 
 | ||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(page_title: page_title(live_action), shot_group: shot_group) |       |> assign(page_title: page_title(live_action), shot_record: shot_record) | ||||||
|       |> display_ammo_group(id) |       |> display_pack(id) | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket} |     {:noreply, socket} | ||||||
|   end |   end | ||||||
| @@ -33,13 +32,13 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|     socket = |     socket = | ||||||
|       socket |       socket | ||||||
|       |> assign(page_title: page_title(live_action)) |       |> assign(page_title: page_title(live_action)) | ||||||
|       |> display_ammo_group(id) |       |> display_pack(id) | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket} |     {:noreply, socket} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp page_title(:add_shot_group), do: gettext("Record Shots") |   defp page_title(:add_shot_record), do: gettext("Record Shots") | ||||||
|   defp page_title(:edit_shot_group), do: gettext("Edit Shot Records") |   defp page_title(:edit_shot_record), do: gettext("Edit Shot Record") | ||||||
|   defp page_title(:move), do: gettext("Move Ammo") |   defp page_title(:move), do: gettext("Move Ammo") | ||||||
|   defp page_title(:show), do: gettext("Show Ammo") |   defp page_title(:show), do: gettext("Show Ammo") | ||||||
|   defp page_title(:edit), do: gettext("Edit Ammo") |   defp page_title(:edit), do: gettext("Edit Ammo") | ||||||
| @@ -48,12 +47,12 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|   def handle_event( |   def handle_event( | ||||||
|         "delete", |         "delete", | ||||||
|         _params, |         _params, | ||||||
|         %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket |         %{assigns: %{pack: pack, current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     ammo_group |> Ammo.delete_ammo_group!(current_user) |     pack |> Ammo.delete_pack!(current_user) | ||||||
| 
 | 
 | ||||||
|     prompt = dgettext("prompts", "Ammo deleted succesfully") |     prompt = dgettext("prompts", "Ammo deleted succesfully") | ||||||
|     redirect_to = Routes.ammo_group_index_path(socket, :index) |     redirect_to = ~p"/ammo" | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)} |     {:noreply, socket |> put_flash(:info, prompt) |> push_navigate(to: redirect_to)} | ||||||
|   end |   end | ||||||
| @@ -61,31 +60,30 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|   def handle_event( |   def handle_event( | ||||||
|         "toggle_staged", |         "toggle_staged", | ||||||
|         _params, |         _params, | ||||||
|         %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket |         %{assigns: %{pack: pack, current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     {:ok, ammo_group} = |     {:ok, pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user) | ||||||
|       ammo_group |> Ammo.update_ammo_group(%{"staged" => !ammo_group.staged}, current_user) |  | ||||||
| 
 | 
 | ||||||
|     {:noreply, socket |> display_ammo_group(ammo_group)} |     {:noreply, socket |> display_pack(pack)} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "delete_shot_group", |         "delete_shot_record", | ||||||
|         %{"id" => id}, |         %{"id" => id}, | ||||||
|         %{assigns: %{ammo_group: %{id: ammo_group_id}, current_user: current_user}} = socket |         %{assigns: %{pack: %{id: pack_id}, current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     {:ok, _} = |     {:ok, _} = | ||||||
|       ActivityLog.get_shot_group!(id, current_user) |       ActivityLog.get_shot_record!(id, current_user) | ||||||
|       |> ActivityLog.delete_shot_group(current_user) |       |> ActivityLog.delete_shot_record(current_user) | ||||||
| 
 | 
 | ||||||
|     prompt = dgettext("prompts", "Shot records deleted succesfully") |     prompt = dgettext("prompts", "Shot records deleted succesfully") | ||||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_ammo_group(ammo_group_id)} |     {:noreply, socket |> put_flash(:info, prompt) |> display_pack(pack_id)} | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   @spec display_ammo_group(Socket.t(), AmmoGroup.t() | AmmoGroup.id()) :: Socket.t() |   @spec display_pack(Socket.t(), Pack.t() | Pack.id()) :: Socket.t() | ||||||
|   defp display_ammo_group( |   defp display_pack( | ||||||
|          %{assigns: %{current_user: current_user}} = socket, |          %{assigns: %{current_user: current_user}} = socket, | ||||||
|          %AmmoGroup{container_id: container_id} = ammo_group |          %Pack{container_id: container_id} = pack | ||||||
|        ) do |        ) do | ||||||
|     columns = [ |     columns = [ | ||||||
|       %{label: gettext("Rounds shot"), key: :count}, |       %{label: gettext("Rounds shot"), key: :count}, | ||||||
| @@ -94,35 +92,35 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|       %{label: gettext("Actions"), key: :actions, sortable: false} |       %{label: gettext("Actions"), key: :actions, sortable: false} | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     shot_groups = ActivityLog.list_shot_groups_for_ammo_group(ammo_group, current_user) |     shot_records = ActivityLog.list_shot_records(current_user, pack_id: pack.id) | ||||||
| 
 | 
 | ||||||
|     rows = |     rows = | ||||||
|       shot_groups |       shot_records | ||||||
|       |> Enum.map(fn shot_group -> |       |> Enum.map(fn shot_record -> | ||||||
|         ammo_group |> get_table_row_for_shot_group(shot_group, columns) |         pack |> get_table_row_for_shot_record(shot_record, columns) | ||||||
|       end) |       end) | ||||||
| 
 | 
 | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       ammo_group: ammo_group, |       pack: pack, | ||||||
|       original_count: Ammo.get_original_count(ammo_group, current_user), |       original_count: Ammo.get_original_count(pack, current_user), | ||||||
|       percentage_remaining: Ammo.get_percentage_remaining(ammo_group, current_user), |       percentage_remaining: Ammo.get_percentage_remaining(pack, current_user), | ||||||
|       container: container_id && Containers.get_container!(container_id, current_user), |       container: container_id && Containers.get_container!(container_id, current_user), | ||||||
|       shot_groups: shot_groups, |       shot_records: shot_records, | ||||||
|       columns: columns, |       columns: columns, | ||||||
|       rows: rows |       rows: rows | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   defp display_ammo_group(%{assigns: %{current_user: current_user}} = socket, id), |   defp display_pack(%{assigns: %{current_user: current_user}} = socket, id), | ||||||
|     do: display_ammo_group(socket, Ammo.get_ammo_group!(id, current_user)) |     do: display_pack(socket, Ammo.get_pack!(id, current_user)) | ||||||
| 
 | 
 | ||||||
|   @spec display_currency(float()) :: String.t() |   @spec display_currency(float()) :: String.t() | ||||||
|   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) |   defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) | ||||||
| 
 | 
 | ||||||
|   @spec get_table_row_for_shot_group(AmmoGroup.t(), ShotGroup.t(), [map()]) :: map() |   @spec get_table_row_for_shot_record(Pack.t(), ShotRecord.t(), [map()]) :: map() | ||||||
|   defp get_table_row_for_shot_group(ammo_group, %{id: id, date: date} = shot_group, columns) do |   defp get_table_row_for_shot_record(pack, %{id: id, date: date} = shot_record, columns) do | ||||||
|     assigns = %{ammo_group: ammo_group, shot_group: shot_group} |     assigns = %{pack: pack, shot_record: shot_record} | ||||||
| 
 | 
 | ||||||
|     columns |     columns | ||||||
|     |> Map.new(fn %{key: key} -> |     |> Map.new(fn %{key: key} -> | ||||||
| @@ -140,11 +138,11 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|             ~H""" |             ~H""" | ||||||
|             <div class="px-4 py-2 space-x-4 flex justify-center items-center"> |             <div class="px-4 py-2 space-x-4 flex justify-center items-center"> | ||||||
|               <.link |               <.link | ||||||
|                 patch={Routes.ammo_group_show_path(Endpoint, :edit_shot_group, @ammo_group, @shot_group)} |                 patch={~p"/ammo/show/#{@pack}/edit/#{@shot_record}"} | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Edit shot group of %{shot_group_count} shots", |                   dgettext("actions", "Edit shot record of %{shot_record_count} shots", | ||||||
|                     shot_group_count: @shot_group.count |                     shot_record_count: @shot_record.count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -154,12 +152,12 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|               <.link |               <.link | ||||||
|                 href="#" |                 href="#" | ||||||
|                 class="text-primary-600 link" |                 class="text-primary-600 link" | ||||||
|                 phx-click="delete_shot_group" |                 phx-click="delete_shot_record" | ||||||
|                 phx-value-id={@shot_group.id} |                 phx-value-id={@shot_record.id} | ||||||
|                 data-confirm={dgettext("prompts", "Are you sure you want to delete this shot record?")} |                 data-confirm={dgettext("prompts", "Are you sure you want to delete this shot record?")} | ||||||
|                 aria-label={ |                 aria-label={ | ||||||
|                   dgettext("actions", "Delete shot record of %{shot_group_count} shots", |                   dgettext("actions", "Delete shot record of %{shot_record_count} shots", | ||||||
|                     shot_group_count: @shot_group.count |                     shot_record_count: @shot_record.count | ||||||
|                   ) |                   ) | ||||||
|                 } |                 } | ||||||
|               > |               > | ||||||
| @@ -169,7 +167,7 @@ defmodule CanneryWeb.AmmoGroupLive.Show do | |||||||
|             """ |             """ | ||||||
| 
 | 
 | ||||||
|           key -> |           key -> | ||||||
|             shot_group |> Map.get(key) |             shot_record |> Map.get(key) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|       {key, value} |       {key, value} | ||||||
| @@ -1,12 +1,12 @@ | |||||||
| <div class="mx-auto space-y-4 max-w-3xl flex flex-col justify-center items-center"> | <div class="mx-auto space-y-4 max-w-3xl flex flex-col justify-center items-center"> | ||||||
|   <h1 class="title text-2xl title-primary-500"> |   <h1 class="title text-2xl title-primary-500"> | ||||||
|     <%= @ammo_group.ammo_type.name %> |     <%= @pack.type.name %> | ||||||
|   </h1> |   </h1> | ||||||
| 
 | 
 | ||||||
|   <div class="space-y-2 flex flex-col justify-center items-center"> |   <div class="space-y-2 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 %> |       <%= @pack.count %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <span class="rounded-lg title text-lg"> |     <span class="rounded-lg title text-lg"> | ||||||
| @@ -19,28 +19,28 @@ | |||||||
|       <%= gettext("%{percentage}%", percentage: @percentage_remaining) %> |       <%= gettext("%{percentage}%", percentage: @percentage_remaining) %> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <%= if @ammo_group.notes do %> |     <%= if @pack.notes do %> | ||||||
|       <span class="rounded-lg title text-lg"> |       <span class="rounded-lg title text-lg"> | ||||||
|         <%= gettext("Notes:") %> |         <%= gettext("Notes:") %> | ||||||
|         <%= @ammo_group.notes %> |         <%= @pack.notes %> | ||||||
|       </span> |       </span> | ||||||
|     <% end %> |     <% end %> | ||||||
| 
 | 
 | ||||||
|     <span class="rounded-lg title text-lg"> |     <span class="rounded-lg title text-lg"> | ||||||
|       <%= gettext("Purchased on:") %> |       <%= gettext("Purchased on:") %> | ||||||
|       <.date id={"#{@ammo_group.id}-purchased-on"} date={@ammo_group.purchased_on} /> |       <.date id={"#{@pack.id}-purchased-on"} date={@pack.purchased_on} /> | ||||||
|     </span> |     </span> | ||||||
| 
 | 
 | ||||||
|     <%= if @ammo_group.price_paid do %> |     <%= if @pack.price_paid do %> | ||||||
|       <span class="rounded-lg title text-lg"> |       <span class="rounded-lg title text-lg"> | ||||||
|         <%= gettext("Original cost:") %> |         <%= gettext("Original cost:") %> | ||||||
|         <%= gettext("$%{amount}", amount: display_currency(@ammo_group.price_paid)) %> |         <%= gettext("$%{amount}", amount: display_currency(@pack.price_paid)) %> | ||||||
|       </span> |       </span> | ||||||
| 
 | 
 | ||||||
|       <span class="rounded-lg title text-lg"> |       <span class="rounded-lg title text-lg"> | ||||||
|         <%= gettext("Current value:") %> |         <%= gettext("Current value:") %> | ||||||
|         <%= gettext("$%{amount}", |         <%= gettext("$%{amount}", | ||||||
|           amount: display_currency(@ammo_group.price_paid * @percentage_remaining / 100) |           amount: display_currency(@pack.price_paid * @percentage_remaining / 100) | ||||||
|         ) %> |         ) %> | ||||||
|       </span> |       </span> | ||||||
|     <% end %> |     <% end %> | ||||||
| @@ -48,20 +48,15 @@ | |||||||
| 
 | 
 | ||||||
|   <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"> | ||||||
|       <.link |       <.link navigate={~p"/type/#{@pack.type}"} class="mx-4 my-2 btn btn-primary"> | ||||||
|         navigate={Routes.ammo_type_show_path(Endpoint, :show, @ammo_group.ammo_type)} |  | ||||||
|         class="mx-4 my-2 btn btn-primary" |  | ||||||
|       > |  | ||||||
|         <%= dgettext("actions", "View in Catalog") %> |         <%= dgettext("actions", "View in Catalog") %> | ||||||
|       </.link> |       </.link> | ||||||
| 
 | 
 | ||||||
|       <.link |       <.link | ||||||
|         patch={Routes.ammo_group_show_path(Endpoint, :edit, @ammo_group)} |         patch={~p"/ammo/show/edit/#{@pack}"} | ||||||
|         class="mx-4 my-2 text-primary-600 link" |         class="mx-4 my-2 text-primary-600 link" | ||||||
|         aria-label={ |         aria-label={ | ||||||
|           dgettext("actions", "Edit ammo group of %{ammo_group_count} bullets", |           dgettext("actions", "Edit pack of %{pack_count} bullets", pack_count: @pack.count) | ||||||
|             ammo_group_count: @ammo_group.count |  | ||||||
|           ) |  | ||||||
|         } |         } | ||||||
|       > |       > | ||||||
|         <i class="fa-fw fa-lg fas fa-edit"></i> |         <i class="fa-fw fa-lg fas fa-edit"></i> | ||||||
| @@ -73,9 +68,7 @@ | |||||||
|         phx-click="delete" |         phx-click="delete" | ||||||
|         data-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?")} | ||||||
|         aria-label={ |         aria-label={ | ||||||
|           dgettext("actions", "Delete ammo group of %{ammo_group_count} bullets", |           dgettext("actions", "Delete pack of %{pack_count} bullets", pack_count: @pack.count) | ||||||
|             ammo_group_count: @ammo_group.count |  | ||||||
|           ) |  | ||||||
|         } |         } | ||||||
|       > |       > | ||||||
|         <i class="fa-fw fa-lg fas fa-trash"></i> |         <i class="fa-fw fa-lg fas fa-trash"></i> | ||||||
| @@ -84,22 +77,16 @@ | |||||||
| 
 | 
 | ||||||
|     <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 @pack.staged, | ||||||
|           do: dgettext("actions", "Unstage from range"), |           do: dgettext("actions", "Unstage from range"), | ||||||
|           else: dgettext("actions", "Stage for range") %> |           else: dgettext("actions", "Stage for range") %> | ||||||
|       </button> |       </button> | ||||||
| 
 | 
 | ||||||
|       <.link |       <.link patch={~p"/ammo/show/move/#{@pack}"} class="btn btn-primary"> | ||||||
|         patch={Routes.ammo_group_show_path(Endpoint, :move, @ammo_group)} |  | ||||||
|         class="btn btn-primary" |  | ||||||
|       > |  | ||||||
|         <%= dgettext("actions", "Move ammo") %> |         <%= dgettext("actions", "Move ammo") %> | ||||||
|       </.link> |       </.link> | ||||||
| 
 | 
 | ||||||
|       <.link |       <.link patch={~p"/ammo/show/add_shot_record/#{@pack}"} class="mx-4 my-2 btn btn-primary"> | ||||||
|         patch={Routes.ammo_group_show_path(Endpoint, :add_shot_group, @ammo_group)} |  | ||||||
|         class="mx-4 my-2 btn btn-primary" |  | ||||||
|       > |  | ||||||
|         <%= dgettext("actions", "Record shots") %> |         <%= dgettext("actions", "Record shots") %> | ||||||
|       </.link> |       </.link> | ||||||
|     </div> |     </div> | ||||||
| @@ -119,7 +106,7 @@ | |||||||
|     <% end %> |     <% end %> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   <%= unless @shot_groups |> Enum.empty?() do %> |   <%= unless @shot_records |> Enum.empty?() do %> | ||||||
|     <hr class="mb-4 w-full" /> |     <hr class="mb-4 w-full" /> | ||||||
| 
 | 
 | ||||||
|     <h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl"> |     <h1 class="mb-4 px-4 py-2 text-center rounded-lg title text-xl"> | ||||||
| @@ -128,7 +115,7 @@ | |||||||
| 
 | 
 | ||||||
|     <.live_component |     <.live_component | ||||||
|       module={CanneryWeb.Components.TableComponent} |       module={CanneryWeb.Components.TableComponent} | ||||||
|       id="ammo_group_shot_groups_table" |       id="pack-shot-records-table" | ||||||
|       columns={@columns} |       columns={@columns} | ||||||
|       rows={@rows} |       rows={@rows} | ||||||
|     /> |     /> | ||||||
| @@ -137,50 +124,50 @@ | |||||||
| 
 | 
 | ||||||
| <%= case @live_action do %> | <%= case @live_action do %> | ||||||
|   <% :edit -> %> |   <% :edit -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}> |     <.modal return_to={~p"/ammo/show/#{@pack}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.AmmoGroupLive.FormComponent} |         module={CanneryWeb.PackLive.FormComponent} | ||||||
|         id={@ammo_group.id} |         id={@pack.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} |         return_to={~p"/ammo/show/#{@pack}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :edit_shot_group -> %> |   <% :edit_shot_record -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}> |     <.modal return_to={~p"/ammo/show/#{@pack}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.RangeLive.FormComponent} |         module={CanneryWeb.RangeLive.FormComponent} | ||||||
|         id={@shot_group.id} |         id={@shot_record.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         shot_group={@shot_group} |         shot_record={@shot_record} | ||||||
|         return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} |         return_to={~p"/ammo/show/#{@pack}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :add_shot_group -> %> |   <% :add_shot_record -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}> |     <.modal return_to={~p"/ammo/show/#{@pack}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.AddShotGroupComponent} |         module={CanneryWeb.Components.AddShotRecordComponent} | ||||||
|         id={:new} |         id={:new} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} |         return_to={~p"/ammo/show/#{@pack}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :move -> %> |   <% :move -> %> | ||||||
|     <.modal return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)}> |     <.modal return_to={~p"/ammo/show/#{@pack}"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.MoveAmmoGroupComponent} |         module={CanneryWeb.Components.MovePackComponent} | ||||||
|         id={@ammo_group.id} |         id={@pack.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} |         return_to={~p"/ammo/show/#{@pack}"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
| @@ -1,56 +1,56 @@ | |||||||
| defmodule CanneryWeb.RangeLive.FormComponent do | defmodule CanneryWeb.RangeLive.FormComponent do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Livecomponent that can update a ShotGroup |   Livecomponent that can update a ShotRecord | ||||||
|   """ |   """ | ||||||
|  |  | ||||||
|   use CanneryWeb, :live_component |   use CanneryWeb, :live_component | ||||||
|   alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup} |   alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotRecord, Ammo, Ammo.Pack} | ||||||
|   alias Ecto.Changeset |   alias Ecto.Changeset | ||||||
|   alias Phoenix.LiveView.Socket |   alias Phoenix.LiveView.Socket | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   def mount(socket), do: {:ok, socket |> assign(:ammo_group, nil)} |   def mount(socket), do: {:ok, socket |> assign(:pack, nil)} | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   @spec update( |   @spec update( | ||||||
|           %{ |           %{ | ||||||
|             required(:shot_group) => ShotGroup.t(), |             required(:shot_record) => ShotRecord.t(), | ||||||
|             required(:current_user) => User.t(), |             required(:current_user) => User.t(), | ||||||
|             optional(:ammo_group) => AmmoGroup.t(), |             optional(:pack) => Pack.t(), | ||||||
|             optional(any()) => any() |             optional(any()) => any() | ||||||
|           }, |           }, | ||||||
|           Socket.t() |           Socket.t() | ||||||
|         ) :: {:ok, Socket.t()} |         ) :: {:ok, Socket.t()} | ||||||
|   def update( |   def update( | ||||||
|         %{ |         %{ | ||||||
|           shot_group: %ShotGroup{ammo_group_id: ammo_group_id}, |           shot_record: %ShotRecord{pack_id: pack_id}, | ||||||
|           current_user: current_user |           current_user: current_user | ||||||
|         } = assigns, |         } = assigns, | ||||||
|         socket |         socket | ||||||
|       ) |       ) | ||||||
|       when is_binary(ammo_group_id) do |       when is_binary(pack_id) do | ||||||
|     ammo_group = Ammo.get_ammo_group!(ammo_group_id, current_user) |     pack = Ammo.get_pack!(pack_id, current_user) | ||||||
|     {:ok, socket |> assign(assigns) |> assign(:ammo_group, ammo_group) |> assign_changeset(%{})} |     {:ok, socket |> assign(assigns) |> assign(:pack, pack) |> assign_changeset(%{})} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def update(%{shot_group: %ShotGroup{}} = assigns, socket) do |   def update(%{shot_record: %ShotRecord{}} = assigns, socket) do | ||||||
|     {:ok, socket |> assign(assigns) |> assign_changeset(%{})} |     {:ok, socket |> assign(assigns) |> assign_changeset(%{})} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   def handle_event("validate", %{"shot_group" => shot_group_params}, socket) do |   def handle_event("validate", %{"shot_record" => shot_record_params}, socket) do | ||||||
|     {:noreply, socket |> assign_changeset(shot_group_params, :validate)} |     {:noreply, socket |> assign_changeset(shot_record_params, :validate)} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "save", |         "save", | ||||||
|         %{"shot_group" => shot_group_params}, |         %{"shot_record" => shot_record_params}, | ||||||
|         %{assigns: %{shot_group: shot_group, current_user: current_user, return_to: return_to}} = |         %{assigns: %{shot_record: shot_record, current_user: current_user, return_to: return_to}} = | ||||||
|           socket |           socket | ||||||
|       ) do |       ) do | ||||||
|     socket = |     socket = | ||||||
|       case ActivityLog.update_shot_group(shot_group, shot_group_params, current_user) do |       case ActivityLog.update_shot_record(shot_record, shot_record_params, current_user) do | ||||||
|         {:ok, _shot_group} -> |         {:ok, _shot_record} -> | ||||||
|           prompt = dgettext("prompts", "Shot records updated successfully") |           prompt = dgettext("prompts", "Shot records updated successfully") | ||||||
|           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) |           socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) | ||||||
|  |  | ||||||
| @@ -66,23 +66,23 @@ defmodule CanneryWeb.RangeLive.FormComponent do | |||||||
|            assigns: %{ |            assigns: %{ | ||||||
|              action: live_action, |              action: live_action, | ||||||
|              current_user: user, |              current_user: user, | ||||||
|              ammo_group: ammo_group, |              pack: pack, | ||||||
|              shot_group: shot_group |              shot_record: shot_record | ||||||
|            } |            } | ||||||
|          } = socket, |          } = socket, | ||||||
|          shot_group_params, |          shot_record_params, | ||||||
|          action \\ nil |          action \\ nil | ||||||
|        ) do |        ) do | ||||||
|     default_action = |     default_action = | ||||||
|       case live_action do |       case live_action do | ||||||
|         :add_shot_group -> :insert |         :add_shot_record -> :insert | ||||||
|         editing when editing in [:edit, :edit_shot_group] -> :update |         editing when editing in [:edit, :edit_shot_record] -> :update | ||||||
|       end |       end | ||||||
|  |  | ||||||
|     changeset = |     changeset = | ||||||
|       case default_action do |       case default_action do | ||||||
|         :insert -> shot_group |> ShotGroup.create_changeset(user, ammo_group, shot_group_params) |         :insert -> shot_record |> ShotRecord.create_changeset(user, pack, shot_record_params) | ||||||
|         :update -> shot_group |> ShotGroup.update_changeset(user, shot_group_params) |         :update -> shot_record |> ShotRecord.update_changeset(user, shot_record_params) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|     changeset = |     changeset = | ||||||
|   | |||||||
| @@ -6,14 +6,14 @@ | |||||||
|   <.form |   <.form | ||||||
|     :let={f} |     :let={f} | ||||||
|     for={@changeset} |     for={@changeset} | ||||||
|     id="shot-group-form" |     id="shot-record-form" | ||||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" |     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||||
|     phx-target={@myself} |     phx-target={@myself} | ||||||
|     phx-change="validate" |     phx-change="validate" | ||||||
|     phx-submit="save" |     phx-submit="save" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       :if={@changeset.action && not @changeset.valid?()} |       :if={@changeset.action && not @changeset.valid?} | ||||||
|       class="invalid-feedback col-span-3 text-center" |       class="invalid-feedback col-span-3 text-center" | ||||||
|     > |     > | ||||||
|       <%= changeset_errors(@changeset) %> |       <%= changeset_errors(@changeset) %> | ||||||
| @@ -22,18 +22,17 @@ | |||||||
|     <%= label(f, :count, gettext("Shots fired"), class: "title text-lg text-primary-600") %> |     <%= label(f, :count, gettext("Shots fired"), class: "title text-lg text-primary-600") %> | ||||||
|     <%= number_input(f, :count, |     <%= number_input(f, :count, | ||||||
|       min: 1, |       min: 1, | ||||||
|       max: @shot_group.count + @ammo_group.count, |       max: @shot_record.count + @pack.count, | ||||||
|       class: "input input-primary col-span-2" |       class: "input input-primary col-span-2" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :count, "col-span-3") %> |     <%= error_tag(f, :count, "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") %> | ||||||
|     <%= textarea(f, :notes, |     <%= textarea(f, :notes, | ||||||
|       id: "shot-group-form-notes", |       id: "shot-record-form-notes", | ||||||
|       class: "input input-primary col-span-2", |       class: "input input-primary col-span-2", | ||||||
|       maxlength: 255, |       maxlength: 255, | ||||||
|       placeholder: gettext("Really great weather"), |       placeholder: gettext("Really great weather"), | ||||||
|       phx_hook: "MaintainAttrs", |  | ||||||
|       phx_update: "ignore" |       phx_update: "ignore" | ||||||
|     ) %> |     ) %> | ||||||
|     <%= error_tag(f, :notes, "col-span-3") %> |     <%= error_tag(f, :notes, "col-span-3") %> | ||||||
|   | |||||||
| @@ -1,20 +1,19 @@ | |||||||
| defmodule CanneryWeb.RangeLive.Index do | defmodule CanneryWeb.RangeLive.Index do | ||||||
|   @moduledoc """ |   @moduledoc """ | ||||||
|   Main page for range day mode, where `AmmoGroup`s can be used up. |   Main page for range day mode, where `Pack`s can be used up. | ||||||
|   """ |   """ | ||||||
|  |  | ||||||
|   use CanneryWeb, :live_view |   use CanneryWeb, :live_view | ||||||
|   alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo} |   alias Cannery.{ActivityLog, ActivityLog.ShotRecord, Ammo} | ||||||
|   alias CanneryWeb.Endpoint |  | ||||||
|   alias Phoenix.LiveView.Socket |   alias Phoenix.LiveView.Socket | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   def mount(%{"search" => search}, _session, socket) do |   def mount(%{"search" => search}, _session, socket) do | ||||||
|     {:ok, socket |> assign(type: :all, search: search) |> display_shot_groups()} |     {:ok, socket |> assign(class: :all, search: search) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def mount(_params, _session, socket) do |   def mount(_params, _session, socket) do | ||||||
|     {:ok, socket |> assign(type: :all, search: nil) |> display_shot_groups()} |     {:ok, socket |> assign(class: :all, search: nil) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
| @@ -24,125 +23,126 @@ defmodule CanneryWeb.RangeLive.Index do | |||||||
|  |  | ||||||
|   defp apply_action( |   defp apply_action( | ||||||
|          %{assigns: %{current_user: current_user}} = socket, |          %{assigns: %{current_user: current_user}} = socket, | ||||||
|          :add_shot_group, |          :add_shot_record, | ||||||
|          %{"id" => id} |          %{"id" => id} | ||||||
|        ) do |        ) do | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Record Shots"), |       page_title: gettext("Record Shots"), | ||||||
|       ammo_group: Ammo.get_ammo_group!(id, current_user) |       pack: Ammo.get_pack!(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( |     |> assign( | ||||||
|       page_title: gettext("Edit Shot Records"), |       page_title: gettext("Edit Shot Record"), | ||||||
|       shot_group: ActivityLog.get_shot_group!(id, current_user) |       shot_record: ActivityLog.get_shot_record!(id, current_user) | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp apply_action(socket, :new, _params) do |   defp apply_action(socket, :new, _params) do | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("New Shot Records"), |       page_title: gettext("Record Shots"), | ||||||
|       shot_group: %ShotGroup{} |       shot_record: %ShotRecord{} | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp apply_action(socket, :index, _params) do |   defp apply_action(socket, :index, _params) do | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Shot Records"), |       page_title: gettext("Range"), | ||||||
|       search: nil, |       search: nil, | ||||||
|       shot_group: nil |       shot_record: nil | ||||||
|     ) |     ) | ||||||
|     |> display_shot_groups() |     |> display_shot_records() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   defp apply_action(socket, :search, %{"search" => search}) do |   defp apply_action(socket, :search, %{"search" => search}) do | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       page_title: gettext("Shot Records"), |       page_title: gettext("Range"), | ||||||
|       search: search, |       search: search, | ||||||
|       shot_group: nil |       shot_record: nil | ||||||
|     ) |     ) | ||||||
|     |> display_shot_groups() |     |> display_shot_records() | ||||||
|   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 | ||||||
|     {:ok, _} = |     {:ok, _} = | ||||||
|       ActivityLog.get_shot_group!(id, current_user) |       ActivityLog.get_shot_record!(id, current_user) | ||||||
|       |> ActivityLog.delete_shot_group(current_user) |       |> ActivityLog.delete_shot_record(current_user) | ||||||
|  |  | ||||||
|     prompt = dgettext("prompts", "Shot records deleted succesfully") |     prompt = dgettext("prompts", "Shot records deleted succesfully") | ||||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_shot_groups()} |     {:noreply, socket |> put_flash(:info, prompt) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event( |   def handle_event( | ||||||
|         "toggle_staged", |         "toggle_staged", | ||||||
|         %{"ammo_group_id" => ammo_group_id}, |         %{"pack_id" => pack_id}, | ||||||
|         %{assigns: %{current_user: current_user}} = socket |         %{assigns: %{current_user: current_user}} = socket | ||||||
|       ) do |       ) do | ||||||
|     ammo_group = Ammo.get_ammo_group!(ammo_group_id, current_user) |     pack = Ammo.get_pack!(pack_id, current_user) | ||||||
|  |  | ||||||
|     {:ok, _ammo_group} = |     {:ok, _pack} = pack |> Ammo.update_pack(%{"staged" => !pack.staged}, current_user) | ||||||
|       ammo_group |> Ammo.update_ammo_group(%{"staged" => !ammo_group.staged}, current_user) |  | ||||||
|  |  | ||||||
|     prompt = dgettext("prompts", "Ammo 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_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do | ||||||
|     {:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :index))} |     {:noreply, socket |> push_patch(to: ~p"/range")} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do |   def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do | ||||||
|     {:noreply, socket |> push_patch(to: Routes.range_index_path(Endpoint, :search, search_term))} |     {:noreply, socket |> push_patch(to: ~p"/range/search/#{search_term}")} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "rifle"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "rifle"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :rifle) |> display_shot_groups()} |     {:noreply, socket |> assign(:class, :rifle) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "shotgun"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "shotgun"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :shotgun) |> display_shot_groups()} |     {:noreply, socket |> assign(:class, :shotgun) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => "pistol"}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => "pistol"}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :pistol) |> display_shot_groups()} |     {:noreply, socket |> assign(:class, :pistol) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_event("change_type", %{"ammo_type" => %{"type" => _all}}, socket) do |   def handle_event("change_class", %{"type" => %{"class" => _all}}, socket) do | ||||||
|     {:noreply, socket |> assign(:type, :all) |> display_shot_groups()} |     {:noreply, socket |> assign(:class, :all) |> display_shot_records()} | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec display_shot_groups(Socket.t()) :: Socket.t() |   @spec display_shot_records(Socket.t()) :: Socket.t() | ||||||
|   defp display_shot_groups( |   defp display_shot_records( | ||||||
|          %{assigns: %{type: type, search: search, current_user: current_user}} = socket |          %{assigns: %{class: class, search: search, current_user: current_user}} = socket | ||||||
|        ) do |        ) do | ||||||
|     shot_groups = ActivityLog.list_shot_groups(search, type, current_user) |     shot_records = ActivityLog.list_shot_records(current_user, search: search, class: class) | ||||||
|     ammo_groups = Ammo.list_staged_ammo_groups(current_user) |     packs = Ammo.list_packs(current_user, staged: true) | ||||||
|     chart_data = shot_groups |> get_chart_data_for_shot_group() |     chart_data = shot_records |> get_chart_data_for_shot_record() | ||||||
|     original_counts = ammo_groups |> Ammo.get_original_counts(current_user) |     original_counts = packs |> Ammo.get_original_counts(current_user) | ||||||
|     cprs = ammo_groups |> Ammo.get_cprs(current_user) |     cprs = packs |> Ammo.get_cprs(current_user) | ||||||
|     last_used_dates = ammo_groups |> ActivityLog.get_last_used_dates(current_user) |     last_used_dates = packs |> ActivityLog.get_last_used_dates(current_user) | ||||||
|  |     shot_record_count = ActivityLog.get_shot_record_count!(current_user) | ||||||
|  |  | ||||||
|     socket |     socket | ||||||
|     |> assign( |     |> assign( | ||||||
|       ammo_groups: ammo_groups, |       packs: packs, | ||||||
|       original_counts: original_counts, |       original_counts: original_counts, | ||||||
|       cprs: cprs, |       cprs: cprs, | ||||||
|       last_used_dates: last_used_dates, |       last_used_dates: last_used_dates, | ||||||
|       chart_data: chart_data, |       chart_data: chart_data, | ||||||
|       shot_groups: shot_groups |       shot_records: shot_records, | ||||||
|  |       shot_record_count: shot_record_count | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @spec get_chart_data_for_shot_group([ShotGroup.t()]) :: [map()] |   @spec get_chart_data_for_shot_record([ShotRecord.t()]) :: [map()] | ||||||
|   defp get_chart_data_for_shot_group(shot_groups) do |   defp get_chart_data_for_shot_record(shot_records) do | ||||||
|     shot_groups |     shot_records | ||||||
|     |> Enum.group_by(fn %{date: date} -> date end, fn %{count: count} -> count end) |     |> Enum.group_by(fn %{date: date} -> date end, fn %{count: count} -> count end) | ||||||
|     |> Enum.map(fn {date, rounds} -> |     |> Enum.map(fn {date, rounds} -> | ||||||
|       sum = Enum.sum(rounds) |       sum = Enum.sum(rounds) | ||||||
|   | |||||||
| @@ -3,54 +3,51 @@ | |||||||
|     <%= gettext("Range day") %> |     <%= gettext("Range day") %> | ||||||
|   </h1> |   </h1> | ||||||
|  |  | ||||||
|   <%= if @ammo_groups |> Enum.empty?() do %> |   <%= if @packs |> Enum.empty?() do %> | ||||||
|     <h1 class="title text-xl text-primary-600"> |     <h1 class="title text-xl text-primary-600"> | ||||||
|       <%= gettext("No ammo staged") %> |       <%= gettext("No ammo staged") %> | ||||||
|       <%= display_emoji("😔") %> |       <%= display_emoji("😔") %> | ||||||
|     </h1> |     </h1> | ||||||
|  |  | ||||||
|     <.link navigate={Routes.ammo_group_index_path(Endpoint, :index)} class="btn btn-primary"> |     <.link navigate={~p"/ammo"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Why not get some ready to shoot?") %> |       <%= dgettext("actions", "Why not get some ready to shoot?") %> | ||||||
|     </.link> |     </.link> | ||||||
|   <% else %> |   <% else %> | ||||||
|     <.link navigate={Routes.ammo_group_index_path(Endpoint, :index)} class="btn btn-primary"> |     <.link navigate={~p"/ammo"} class="btn btn-primary"> | ||||||
|       <%= dgettext("actions", "Stage ammo") %> |       <%= dgettext("actions", "Stage ammo") %> | ||||||
|     </.link> |     </.link> | ||||||
|  |  | ||||||
|     <div class="w-full flex flex-row flex-wrap justify-center items-stretch"> |     <div class="w-full flex flex-row flex-wrap justify-center items-stretch"> | ||||||
|       <.ammo_group_card |       <.pack_card | ||||||
|         :for={%{id: ammo_group_id} = ammo_group <- @ammo_groups} |         :for={%{id: pack_id} = pack <- @packs} | ||||||
|         ammo_group={ammo_group} |         pack={pack} | ||||||
|         original_count={Map.fetch!(@original_counts, ammo_group_id)} |         original_count={Map.fetch!(@original_counts, pack_id)} | ||||||
|         cpr={Map.get(@cprs, ammo_group_id)} |         cpr={Map.get(@cprs, pack_id)} | ||||||
|         last_used_date={Map.get(@last_used_dates, ammo_group_id)} |         last_used_date={Map.get(@last_used_dates, pack_id)} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       > |       > | ||||||
|         <button |         <button | ||||||
|           type="button" |           type="button" | ||||||
|           class="btn btn-primary" |           class="btn btn-primary" | ||||||
|           phx-click="toggle_staged" |           phx-click="toggle_staged" | ||||||
|           phx-value-ammo_group_id={ammo_group.id} |           phx-value-pack_id={pack.id} | ||||||
|           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 pack.staged, | ||||||
|             do: dgettext("actions", "Unstage from range"), |             do: dgettext("actions", "Unstage from range"), | ||||||
|             else: dgettext("actions", "Stage for range") %> |             else: dgettext("actions", "Stage for range") %> | ||||||
|         </button> |         </button> | ||||||
|  |  | ||||||
|         <.link |         <.link patch={~p"/range/add_shot_record/#{pack}"} class="btn btn-primary"> | ||||||
|           patch={Routes.range_index_path(Endpoint, :add_shot_group, ammo_group)} |  | ||||||
|           class="btn btn-primary" |  | ||||||
|         > |  | ||||||
|           <%= dgettext("actions", "Record shots") %> |           <%= dgettext("actions", "Record shots") %> | ||||||
|         </.link> |         </.link> | ||||||
|       </.ammo_group_card> |       </.pack_card> | ||||||
|     </div> |     </div> | ||||||
|   <% end %> |   <% end %> | ||||||
|  |  | ||||||
|   <hr class="hr" /> |   <hr class="hr" /> | ||||||
|  |  | ||||||
|   <%= if @shot_groups |> Enum.empty?() and @search |> is_nil() do %> |   <%= if @shot_record_count == 0 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("😔") %> | ||||||
| @@ -78,16 +75,18 @@ | |||||||
|       <.form |       <.form | ||||||
|         :let={f} |         :let={f} | ||||||
|         for={%{}} |         for={%{}} | ||||||
|         as={:ammo_type} |         as={:type} | ||||||
|         phx-change="change_type" |         phx-change="change_class" | ||||||
|         phx-submit="change_type" |         phx-submit="change_class" | ||||||
|         class="flex items-center" |         class="flex items-center" | ||||||
|       > |       > | ||||||
|         <%= label(f, :type, gettext("Type"), class: "title text-primary-600 text-lg text-center") %> |         <%= label(f, :class, gettext("Class"), | ||||||
|  |           class: "title text-primary-600 text-lg text-center" | ||||||
|  |         ) %> | ||||||
|  |  | ||||||
|         <%= select( |         <%= select( | ||||||
|           f, |           f, | ||||||
|           :type, |           :class, | ||||||
|           [ |           [ | ||||||
|             {gettext("All"), :all}, |             {gettext("All"), :all}, | ||||||
|             {gettext("Rifle"), :rifle}, |             {gettext("Rifle"), :rifle}, | ||||||
| @@ -95,7 +94,7 @@ | |||||||
|             {gettext("Pistol"), :pistol} |             {gettext("Pistol"), :pistol} | ||||||
|           ], |           ], | ||||||
|           class: "mx-2 my-1 min-w-md input input-primary", |           class: "mx-2 my-1 min-w-md input input-primary", | ||||||
|           value: @type |           value: @class | ||||||
|         ) %> |         ) %> | ||||||
|       </.form> |       </.form> | ||||||
|  |  | ||||||
| @@ -117,26 +116,26 @@ | |||||||
|       </.form> |       </.form> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <%= if @shot_groups |> Enum.empty?() do %> |     <%= if @shot_records |> Enum.empty?() 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("😔") %> | ||||||
|       </h1> |       </h1> | ||||||
|     <% else %> |     <% else %> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.ShotGroupTableComponent} |         module={CanneryWeb.Components.ShotRecordTableComponent} | ||||||
|         id="shot_groups_index_table" |         id="shot-records-index-table" | ||||||
|         shot_groups={@shot_groups} |         shot_records={@shot_records} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       > |       > | ||||||
|         <:actions :let={shot_group}> |         <:actions :let={shot_record}> | ||||||
|           <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"> | ||||||
|             <.link |             <.link | ||||||
|               patch={Routes.range_index_path(Endpoint, :edit, shot_group)} |               patch={~p"/range/edit/#{shot_record}"} | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Edit shot record of %{shot_group_count} shots", |                 dgettext("actions", "Edit shot record of %{shot_record_count} shots", | ||||||
|                   shot_group_count: shot_group.count |                   shot_record_count: shot_record.count | ||||||
|                 ) |                 ) | ||||||
|               } |               } | ||||||
|             > |             > | ||||||
| @@ -147,13 +146,13 @@ | |||||||
|               href="#" |               href="#" | ||||||
|               class="text-primary-600 link" |               class="text-primary-600 link" | ||||||
|               phx-click="delete" |               phx-click="delete" | ||||||
|               phx-value-id={shot_group.id} |               phx-value-id={shot_record.id} | ||||||
|               data-confirm={ |               data-confirm={ | ||||||
|                 dgettext("prompts", "Are you sure you want to delete this shot record?") |                 dgettext("prompts", "Are you sure you want to delete this shot record?") | ||||||
|               } |               } | ||||||
|               aria-label={ |               aria-label={ | ||||||
|                 dgettext("actions", "Delete shot record of %{shot_group_count} shots", |                 dgettext("actions", "Delete shot record of %{shot_record_count} shots", | ||||||
|                   shot_group_count: shot_group.count |                   shot_record_count: shot_record.count | ||||||
|                 ) |                 ) | ||||||
|               } |               } | ||||||
|             > |             > | ||||||
| @@ -168,26 +167,26 @@ | |||||||
|  |  | ||||||
| <%= case @live_action do %> | <%= case @live_action do %> | ||||||
|   <% :edit -> %> |   <% :edit -> %> | ||||||
|     <.modal return_to={Routes.range_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/range"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.RangeLive.FormComponent} |         module={CanneryWeb.RangeLive.FormComponent} | ||||||
|         id={@shot_group.id} |         id={@shot_record.id} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         shot_group={@shot_group} |         shot_record={@shot_record} | ||||||
|         return_to={Routes.range_index_path(Endpoint, :index)} |         return_to={~p"/range"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   <% :add_shot_group -> %> |   <% :add_shot_record -> %> | ||||||
|     <.modal return_to={Routes.range_index_path(Endpoint, :index)}> |     <.modal return_to={~p"/range"}> | ||||||
|       <.live_component |       <.live_component | ||||||
|         module={CanneryWeb.Components.AddShotGroupComponent} |         module={CanneryWeb.Components.AddShotRecordComponent} | ||||||
|         id={:new} |         id={:new} | ||||||
|         title={@page_title} |         title={@page_title} | ||||||
|         action={@live_action} |         action={@live_action} | ||||||
|         ammo_group={@ammo_group} |         pack={@pack} | ||||||
|         return_to={Routes.range_index_path(Endpoint, :index)} |         return_to={~p"/range"} | ||||||
|         current_user={@current_user} |         current_user={@current_user} | ||||||
|       /> |       /> | ||||||
|     </.modal> |     </.modal> | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user