Compare commits
22 commits
33d36ed713
...
94637a564c
Author | SHA1 | Date | |
---|---|---|---|
94637a564c | |||
10f284da6f | |||
64e310b598 | |||
61796cf985 | |||
dceef941c7 | |||
68d60c120d | |||
6b787297bb | |||
24642d7c67 | |||
8cd984adc5 | |||
e7d93989d3 | |||
a99c5eea35 | |||
ada166fb41 | |||
6a5d2346ff | |||
f28c85e343 | |||
f27df8d676 | |||
ea74a89078 | |||
ce03eaaf2d | |||
3eff955672 | |||
d43daafdb7 | |||
721ba53c15 | |||
75eb9aa316 | |||
085f226cfe |
172 changed files with 2947 additions and 18233 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -35,7 +35,7 @@ npm-debug.log
|
||||||
# Since we are building assets from assets/,
|
# Since we are building assets from assets/,
|
||||||
# we ignore priv/static. You may want to comment
|
# we ignore priv/static. You may want to comment
|
||||||
# this depending on your deployment strategy.
|
# this depending on your deployment strategy.
|
||||||
/priv/static/
|
#/priv/static/
|
||||||
|
|
||||||
# Files matching config/*.secret.exs pattern contain sensitive
|
# Files matching config/*.secret.exs pattern contain sensitive
|
||||||
# data and you should not commit them into version control.
|
# data and you should not commit them into version control.
|
||||||
|
|
31
Dockerfile
Normal file
31
Dockerfile
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# ./Dockerfile
|
||||||
|
|
||||||
|
# Extend from the official Elixir image
|
||||||
|
FROM elixir:1.13.4-otp-25-alpine
|
||||||
|
|
||||||
|
# # install the package postgresql-client to run pg_isready within entrypoint script
|
||||||
|
# RUN apt-get update && \
|
||||||
|
# apt-get install -y postgresql-client
|
||||||
|
|
||||||
|
# Create app directory and copy the Elixir project into it
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install the build tools we'll need
|
||||||
|
RUN apk update && \
|
||||||
|
apk upgrade --no-cache && \
|
||||||
|
apk add --no-cache \
|
||||||
|
build-base && \
|
||||||
|
mix local.rebar --force && \
|
||||||
|
mix local.hex --force
|
||||||
|
|
||||||
|
|
||||||
|
# The environment to build with
|
||||||
|
ENV MIX_ENV=prod
|
||||||
|
|
||||||
|
# Get deps and compile
|
||||||
|
RUN mix do deps.get, deps.compile, compile
|
||||||
|
|
||||||
|
# Start command
|
||||||
|
CMD = ["/app/entrypoint.sh"]
|
132
README.md
132
README.md
|
@ -6,79 +6,73 @@ Written in Elixir & Phoenix LiveView, with Bootstrap v5.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Ability to edit shifts?
|
- [X] ~~*Proper modal to delete shifts?*~~ [2022-08-14]
|
||||||
- [ ] Proper modal to delete shifts?
|
- [ ] move runtime config out of compile-time config files, to move towards supporting releases
|
||||||
- [ ] Allow all-day items for notes, or require hours even for sick days?
|
- [ ] probably need to use `def get_app_config` style functions instead of `@module_var` module variables, ([see this](https://stephenbussey.com/2019/01/03/understanding-compile-time-dependencies-in-elixir-a-bug-hunt.html))
|
||||||
|
- [ ] Update tests, which are probably all way out of date. But I also don't care that much for this project...
|
||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
### New versions
|
I'm using a dumb & simple docker approach to deploying this now. Nothing automated, the basic steps are:
|
||||||
|
|
||||||
When improvements are made, we can update the deployed version like so:
|
1. ensure latest assets are built, digested, and committed to the repo
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# rebuild static assets:
|
||||||
|
rm -rf ./priv/static/*
|
||||||
|
npm --prefix assets run build
|
||||||
|
MIX_ENV=prod mix phx.digest
|
||||||
|
# then do a new commit and push...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. on server, build a new container, and run it
|
||||||
|
|
||||||
|
### Simple dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# ./Dockerfile
|
||||||
|
|
||||||
|
# Extend from the official Elixir image
|
||||||
|
FROM elixir:1.13.4-otp-25-alpine
|
||||||
|
|
||||||
|
# # install the package postgresql-client to run pg_isready within entrypoint script
|
||||||
|
# RUN apt-get update && \
|
||||||
|
# apt-get install -y postgresql-client
|
||||||
|
|
||||||
|
# Copy the entrypoint script
|
||||||
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
# Create app directory and copy the Elixir projects into it
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY ./app /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install the build tools we'll need
|
||||||
|
RUN apk update && \
|
||||||
|
apk upgrade --no-cache && \
|
||||||
|
apk add --no-cache \
|
||||||
|
build-base && \
|
||||||
|
mix local.rebar --force && \
|
||||||
|
mix local.hex --force
|
||||||
|
|
||||||
|
|
||||||
|
# The environment to build with
|
||||||
|
ENV MIX_ENV=prod
|
||||||
|
|
||||||
|
# Get deps and compile
|
||||||
|
RUN mix do deps.get, deps.compile, compile
|
||||||
|
|
||||||
|
# Start command
|
||||||
|
CMD = ["/entrypoint.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple entrypoint script
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd /opt/shift73k
|
#!/bin/ash
|
||||||
git pull
|
|
||||||
mix deps.get --only prod
|
export MIX_ENV="prod"
|
||||||
MIX_ENV=prod mix compile
|
|
||||||
# might not be needed:
|
cd /app
|
||||||
MIX_ENV=prod mix ecto.migrate
|
exec mix ecto.migrate && mix phx.server
|
||||||
# rebuild static assets:
|
|
||||||
rm -rf priv/static/
|
|
||||||
npm run deploy --prefix ./assets
|
|
||||||
MIX_ENV=prod mix phx.digest
|
|
||||||
MIX_ENV=prod mix release --overwrite
|
|
||||||
# test starting it:
|
|
||||||
MIX_ENV=prod _build/prod/rel/shift73k/bin/shift73k start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### systemd unit:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Shift73k service
|
|
||||||
After=local-fs.target network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=runuser
|
|
||||||
Group=runuser
|
|
||||||
WorkingDirectory=/opt/shift73k/_build/prod/rel/shift73k
|
|
||||||
ExecStart=/opt/shift73k/_build/prod/rel/shift73k/bin/shift73k start
|
|
||||||
ExecStop=/opt/shift73k/_build/prod/rel/shift73k/bin/shift73k stop
|
|
||||||
#EnvironmentFile=/etc/default/myApp.env
|
|
||||||
Environment=LANG=en_US.utf8
|
|
||||||
Environment=MIX_ENV=prod
|
|
||||||
#Environment=PORT=4000
|
|
||||||
LimitNOFILE=65535
|
|
||||||
UMask=0027
|
|
||||||
SyslogIdentifier=shift73k
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### nginx config:
|
|
||||||
|
|
||||||
```conf
|
|
||||||
upstream phoenix {
|
|
||||||
server 127.0.0.1:4000 max_fails=5 fail_timeout=60s;
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
location / {
|
|
||||||
allow all;
|
|
||||||
# Proxy Headers
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Cluster-Client-Ip $remote_addr;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_redirect off;
|
|
||||||
# WebSockets
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_pass http://phoenix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@
|
||||||
@import "bs-colors";
|
@import "bs-colors";
|
||||||
|
|
||||||
// Required || Configuration -- CONTINUED
|
// Required || Configuration -- CONTINUED
|
||||||
|
@import "../node_modules/bootstrap/scss/maps";
|
||||||
@import "../node_modules/bootstrap/scss/mixins";
|
@import "../node_modules/bootstrap/scss/mixins";
|
||||||
@import "../node_modules/bootstrap/scss/utilities";
|
@import "../node_modules/bootstrap/scss/utilities";
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,6 @@
|
||||||
// @import "../node_modules/@fontsource/lato/700-italic.css"; /* bold | italic */
|
// @import "../node_modules/@fontsource/lato/700-italic.css"; /* bold | italic */
|
||||||
// @import "../node_modules/@fontsource/lato/900.css"; /* black | normal */
|
// @import "../node_modules/@fontsource/lato/900.css"; /* black | normal */
|
||||||
// @import "../node_modules/@fontsource/lato/900-italic.css"; /* black | italic */
|
// @import "../node_modules/@fontsource/lato/900-italic.css"; /* black | italic */
|
||||||
|
|
||||||
|
/* Bootstrap Icons Font */
|
||||||
|
@import "../node_modules/bootstrap-icons/font/bootstrap-icons.css";
|
||||||
|
|
|
@ -32,6 +32,10 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
||||||
background-color: $navbar-light-color;
|
background-color: $navbar-light-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: $navbar-light-toggler-border-color 0 0 0 $navbar-toggler-focus-width;
|
||||||
|
// var(--bs-navbar-toggler-focus-width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +55,9 @@ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
||||||
background-color: $navbar-dark-color;
|
background-color: $navbar-dark-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: $navbar-dark-toggler-border-color 0 0 0 $navbar-toggler-focus-width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
SVG ICON SYSTEM
|
|
||||||
per https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4
|
|
||||||
*/
|
|
||||||
.icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon svg,
|
|
||||||
.icon img {
|
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon.baseline svg,
|
|
||||||
.icon img {
|
|
||||||
top: 0.125em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
|
@ -4,9 +4,6 @@
|
||||||
/* Load Bootstrap v5 and customizations */
|
/* Load Bootstrap v5 and customizations */
|
||||||
@import "bs-load";
|
@import "bs-load";
|
||||||
|
|
||||||
/*SVG ICON SYSTEM*/
|
|
||||||
@import "svg-icons";
|
|
||||||
|
|
||||||
/* LiveView specific CSS */
|
/* LiveView specific CSS */
|
||||||
@import "phx-liveview";
|
@import "phx-liveview";
|
||||||
|
|
||||||
|
@ -43,7 +40,7 @@
|
||||||
/* style icon */
|
/* style icon */
|
||||||
.inner-addon > .icon {
|
.inner-addon > .icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0.5625rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,3 +65,11 @@
|
||||||
.shift-description p:last-child {
|
.shift-description p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
fix readonly form background
|
||||||
|
*/
|
||||||
|
.form-control[readonly] {
|
||||||
|
background-color: $input-disabled-bg;
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
const togglerBtn = document.getElementById("navbarSupportedContentToggler");
|
const togglerBtn = document.getElementById('navbarSupportedContentToggler');
|
||||||
const navbarContent = document.getElementById("navbarSupportedContent");
|
const navbarContent = document.getElementById('navbarSupportedContent');
|
||||||
|
|
||||||
navbarContent.addEventListener("show.bs.collapse", () => {
|
if (navbarContent != null) {
|
||||||
console.log("opening navbar content");
|
navbarContent.addEventListener('show.bs.collapse', () => {
|
||||||
togglerBtn.classList.toggle("is-active");
|
console.log('opening navbar content');
|
||||||
});
|
togglerBtn.classList.toggle('is-active');
|
||||||
|
});
|
||||||
navbarContent.addEventListener("hide.bs.collapse", () => {
|
|
||||||
console.log("closing navbar content");
|
navbarContent.addEventListener('hide.bs.collapse', () => {
|
||||||
togglerBtn.classList.toggle("is-active");
|
console.log('closing navbar content');
|
||||||
});
|
togglerBtn.classList.toggle('is-active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,84 +1,24 @@
|
||||||
// We need to import the CSS so that webpack will load it.
|
// We import the main SCSS file, which performs all other SCSS imports,
|
||||||
// The MiniCssExtractPlugin is used to separate it out into
|
// and which vite.js will preprocess with sass.
|
||||||
// its own CSS file.
|
import '../css/app.scss'
|
||||||
import "../css/app.scss";
|
|
||||||
|
|
||||||
// Import icons for sprite-loader
|
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||||
// navbar brand icon
|
import 'phoenix_html'
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-week.svg"; // brand
|
|
||||||
// menus etc
|
|
||||||
import "../node_modules/bootstrap-icons/icons/person-circle.svg"; // accounts menu
|
|
||||||
import "../node_modules/bootstrap-icons/icons/person-plus.svg"; // new user / register
|
|
||||||
import "../node_modules/bootstrap-icons/icons/door-open.svg"; // log in
|
|
||||||
import "../node_modules/bootstrap-icons/icons/door-closed.svg"; // log out
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sliders.svg"; // new user / register
|
|
||||||
// forms etc
|
|
||||||
import "../node_modules/bootstrap-icons/icons/at.svg"; // email field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/key.svg"; // new password field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/key-fill.svg"; // pw confirm field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/lock.svg"; // current pw field
|
|
||||||
import "../node_modules/bootstrap-icons/icons/shield.svg"; // role
|
|
||||||
// live tables
|
|
||||||
import "../node_modules/bootstrap-icons/icons/arrow-down-up.svg"; // sort
|
|
||||||
import "../node_modules/bootstrap-icons/icons/funnel.svg"; // filter
|
|
||||||
import "../node_modules/bootstrap-icons/icons/x-circle-fill.svg"; // clear filter
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sort-down-alt.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/sort-up-alt.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/chevron-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/chevron-right.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/pencil.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/trash.svg";
|
|
||||||
// page headers
|
|
||||||
import "../node_modules/bootstrap-icons/icons/shield-lock.svg"; // reset password
|
|
||||||
import "../node_modules/bootstrap-icons/icons/arrow-repeat.svg"; // resend confirmation
|
|
||||||
import "../node_modules/@mdi/svg/svg/head-question-outline.svg"; // forgot password
|
|
||||||
import "../node_modules/bootstrap-icons/icons/people.svg"; // users management
|
|
||||||
// calendar/event icons
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-plus.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-date.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-event.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/calendar2-range.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/clock-history.svg"; // shift template
|
|
||||||
import "../node_modules/bootstrap-icons/icons/tag.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/hourglass.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/map.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/geo.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/justify-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/plus-circle-dotted.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/clipboard-plus.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/star.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/star-fill.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/binoculars.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/binoculars-fill.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/eraser.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/save.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/asterisk.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/card-list.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/file-earmark-spreadsheet.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/box-arrow-in-left.svg";
|
|
||||||
import "../node_modules/bootstrap-icons/icons/link.svg";
|
|
||||||
|
|
||||||
// webpack automatically bundles all modules in your
|
// import Socket for Phoenix Channels
|
||||||
// entry points. Those entry points can be configured
|
|
||||||
// in "webpack.config.js".
|
|
||||||
//
|
|
||||||
// Import deps with the dep name or local files with a relative path, for example:
|
|
||||||
//
|
|
||||||
// import {Socket} from "phoenix"
|
|
||||||
// import socket from "./socket"
|
|
||||||
//
|
|
||||||
import "phoenix_html";
|
|
||||||
import { Socket } from "phoenix";
|
import { Socket } from "phoenix";
|
||||||
|
// import topbar for load progress in live reloading / liveview
|
||||||
import topbar from "topbar";
|
import topbar from "topbar";
|
||||||
|
// import LiveSocket for LiveView
|
||||||
import { LiveSocket } from "phoenix_live_view";
|
import { LiveSocket } from "phoenix_live_view";
|
||||||
|
|
||||||
|
|
||||||
// Bootstrap v5 js imports
|
// Bootstrap v5 js imports
|
||||||
import "bootstrap/js/dist/alert";
|
import 'bootstrap/js/dist/alert';
|
||||||
import "bootstrap/js/dist/collapse";
|
import 'bootstrap/js/dist/collapse';
|
||||||
import "bootstrap/js/dist/dropdown";
|
import 'bootstrap/js/dist/dropdown';
|
||||||
// Bootstrap helpers
|
// Bootstrap helpers
|
||||||
import "./_hamburger-helper";
|
import './_hamburger-helper';
|
||||||
import "./_form-validity";
|
import "./_form-validity";
|
||||||
// Bootstrap-liveview helpers
|
// Bootstrap-liveview helpers
|
||||||
import { AlertRemover } from "./_alert-remover";
|
import { AlertRemover } from "./_alert-remover";
|
||||||
|
|
17561
assets/package-lock.json
generated
17561
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,43 +1,28 @@
|
||||||
{
|
{
|
||||||
"repository": {},
|
"name": "vanilla",
|
||||||
"description": " ",
|
"private": true,
|
||||||
"license": "MIT",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "NODE_ENV=production webpack --mode production",
|
"dev": "vite",
|
||||||
"watch": "webpack --mode development --watch"
|
"build": "vite build",
|
||||||
},
|
"preview": "vite preview"
|
||||||
"dependencies": {
|
|
||||||
"@fontsource/lato": "^4.2.1",
|
|
||||||
"@mdi/svg": "^5.9.55",
|
|
||||||
"@popperjs/core": "^2.8.4",
|
|
||||||
"bootstrap": "^5.0.0-beta3",
|
|
||||||
"bootstrap-icons": "^1.4.0",
|
|
||||||
"hamburgers": "^1.1.3",
|
|
||||||
"heroicons": "^0.4.2",
|
|
||||||
"phoenix": "file:../deps/phoenix",
|
|
||||||
"phoenix_html": "file:../deps/phoenix_html",
|
|
||||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
|
||||||
"topbar": "^1.x"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.x",
|
"@types/node": "^18.6.5",
|
||||||
"@babel/preset-env": "^7.x",
|
"@types/phoenix": "^1.5.4",
|
||||||
"autoprefixer": "^10.2.4",
|
"sass": "^1.54.3",
|
||||||
"babel-loader": "^8.x",
|
"svg-sprite-generator": "^0.0.7",
|
||||||
"copy-webpack-plugin": "^8.x",
|
"vite": "^3.0.0"
|
||||||
"css-loader": "^5.x",
|
},
|
||||||
"css-minimizer-webpack-plugin": "^1.x",
|
"dependencies": {
|
||||||
"file-loader": "^6.2.0",
|
"@fontsource/lato": "^4.5.9",
|
||||||
"glob-all": "^3.2.1",
|
"bootstrap": "^5.2.0",
|
||||||
"mini-css-extract-plugin": "^1.x",
|
"bootstrap-icons": "^1.9.1",
|
||||||
"postcss": "^8.2.6",
|
"hamburgers": "^1.2.1",
|
||||||
"postcss-loader": "^5.0.0",
|
"phoenix": "^1.6.11",
|
||||||
"postcss-scss": "^3.0.4",
|
"phoenix_html": "^3.2.0",
|
||||||
"purgecss-webpack-plugin": "^4.0.2",
|
"phoenix_live_view": "^0.17.11",
|
||||||
"sass": "^1.x",
|
"topbar": "^1.x"
|
||||||
"sass-loader": "^11.x",
|
|
||||||
"svg-sprite-loader": "^6.x",
|
|
||||||
"webpack": "^5.x",
|
|
||||||
"webpack-cli": "^4.x"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: require("postcss-scss"),
|
|
||||||
|
|
||||||
plugins: [require("autoprefixer")],
|
|
||||||
};
|
|
37
assets/vite.config.js
Normal file
37
assets/vite.config.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig(({ command }) => {
|
||||||
|
const isDev = command !== "build";
|
||||||
|
if (isDev) {
|
||||||
|
// Terminate the watcher when Phoenix quits
|
||||||
|
process.stdin.on("close", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
publicDir: "static",
|
||||||
|
build: {
|
||||||
|
target: "esnext", // build for recent browsers
|
||||||
|
outDir: "../priv/static", // emit assets to priv/static
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: isDev, // enable source map in dev build
|
||||||
|
manifest: false, // do not generate manifest.json
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
app: "./js/app.js"
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: "assets/[name].js", // remove hash
|
||||||
|
chunkFileNames: "assets/[name].js",
|
||||||
|
assetFileNames: "assets/[name][extname]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -1,105 +0,0 @@
|
||||||
const path = require("path");
|
|
||||||
const glob = require("glob-all");
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
|
||||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
|
||||||
const SpriteLoaderPlugin = require("svg-sprite-loader/plugin");
|
|
||||||
const PurgecssPlugin = require("purgecss-webpack-plugin");
|
|
||||||
|
|
||||||
module.exports = (env, options) => {
|
|
||||||
const devMode = options.mode !== "production";
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry: {
|
|
||||||
app: glob.sync("./vendor/**/*.js").concat(["./js/app.js"]),
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, "../priv/static/js"),
|
|
||||||
filename: "[name].js",
|
|
||||||
publicPath: "/js/",
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: "babel-loader",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.[s]?css$/,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
"css-loader",
|
|
||||||
"sass-loader",
|
|
||||||
"postcss-loader",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: "file-loader",
|
|
||||||
options: {
|
|
||||||
esModule: false,
|
|
||||||
name: "[name].[ext]",
|
|
||||||
outputPath: "../fonts",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
loader: "svg-sprite-loader",
|
|
||||||
options: {
|
|
||||||
extract: true,
|
|
||||||
spriteFilename: "icons.svg",
|
|
||||||
publicPath: "../images/",
|
|
||||||
symbolId: (filePath) => {
|
|
||||||
if (filePath.includes("bootstrap-icons")) {
|
|
||||||
return `bi-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else if (filePath.includes("@mdi")) {
|
|
||||||
return `mdi-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else if (filePath.includes("heroicons")) {
|
|
||||||
if (filePath.includes("outline")) {
|
|
||||||
return `hio-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
} else {
|
|
||||||
return `his-${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `${path.basename(filePath).slice(0, -4)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new MiniCssExtractPlugin({ filename: "../css/app.css" }),
|
|
||||||
new SpriteLoaderPlugin({ plainSprite: true }),
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [{ from: "static/", to: "../" }],
|
|
||||||
}),
|
|
||||||
].concat(
|
|
||||||
devMode
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
new PurgecssPlugin({
|
|
||||||
paths: glob.sync([
|
|
||||||
"../**/*.html.leex",
|
|
||||||
"../**/*.html.eex",
|
|
||||||
"../**/views/**/*.ex",
|
|
||||||
"../**/live/**/*.ex",
|
|
||||||
"./js/**/*.js",
|
|
||||||
]),
|
|
||||||
safelist: [/phx/, /topbar/],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
optimization: {
|
|
||||||
minimizer: ["...", new CssMinimizerPlugin()],
|
|
||||||
},
|
|
||||||
devtool: devMode ? "source-map" : undefined,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -5,7 +5,7 @@
|
||||||
# is restricted to this project.
|
# is restricted to this project.
|
||||||
|
|
||||||
# General application configuration
|
# General application configuration
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ config :shift73k,
|
||||||
config :shift73k, :app_global_vars,
|
config :shift73k, :app_global_vars,
|
||||||
time_zone: "America/New_York",
|
time_zone: "America/New_York",
|
||||||
mailer_reply_to: "reply_to@example.com",
|
mailer_reply_to: "reply_to@example.com",
|
||||||
mailer_from: "app_name@example.com"
|
mailer_from: "app_name@example.com",
|
||||||
|
allow_registration: :true
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :shift73k, Shift73kWeb.Endpoint,
|
config :shift73k, Shift73kWeb.Endpoint,
|
||||||
|
@ -26,6 +27,18 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
pubsub_server: Shift73k.PubSub,
|
pubsub_server: Shift73k.PubSub,
|
||||||
live_view: [signing_salt: "2D4GC4ac"]
|
live_view: [signing_salt: "2D4GC4ac"]
|
||||||
|
|
||||||
|
# Configures the mailer
|
||||||
|
#
|
||||||
|
# By default it uses the "Local" adapter which stores the emails
|
||||||
|
# locally. You can see the emails in your browser, at "/dev/mailbox".
|
||||||
|
#
|
||||||
|
# For production it's recommended to configure a different adapter
|
||||||
|
# at the `config/runtime.exs`.
|
||||||
|
config :shift73k, Shift73k.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
|
# Swoosh API client is needed for adapters other than SMTP.
|
||||||
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
# Configures Elixir's Logger
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
format: "$time $metadata[$level] $message\n",
|
format: "$time $metadata[$level] $message\n",
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :shift73k, Shift73k.Repo,
|
config :shift73k, Shift73k.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
|
socket_dir: "/srv/dck/postgres/sock/postgres",
|
||||||
database: "shift73k_dev",
|
database: "shift73k_dev",
|
||||||
hostname: "localhost",
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true,
|
show_sensitive_data_on_connection_error: true,
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
|
@ -22,11 +23,7 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
node: [
|
node: [
|
||||||
"node_modules/webpack/bin/webpack.js",
|
"node_modules/vite/bin/vite.js",
|
||||||
"--mode",
|
|
||||||
"development",
|
|
||||||
"--watch",
|
|
||||||
"--watch-options-stdin",
|
|
||||||
cd: Path.expand("../assets", __DIR__)
|
cd: Path.expand("../assets", __DIR__)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# For production, don't forget to configure the url host
|
# For production, don't forget to configure the url host
|
||||||
# to something meaningful, Phoenix uses this information
|
# to something meaningful, Phoenix uses this information
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
# Only in tests, remove the complexity from the password hashing algorithm
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
config :bcrypt_elixir, :log_rounds, 1
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
@ -13,7 +13,8 @@ config :shift73k, Shift73k.Repo,
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
database: "shift73k_test#{System.get_env("MIX_TEST_PARTITION")}",
|
database: "shift73k_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
pool: Ecto.Adapters.SQL.Sandbox
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
|
pool_size: 10
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
# you can enable the server option below.
|
# you can enable the server option below.
|
||||||
|
@ -24,8 +25,8 @@ config :shift73k, Shift73kWeb.Endpoint,
|
||||||
# Print only warnings and errors during test
|
# Print only warnings and errors during test
|
||||||
config :logger, level: :warn
|
config :logger, level: :warn
|
||||||
|
|
||||||
# Bamboo test mailer config
|
# Swoosh test mailer config
|
||||||
config :shift73k, Shift73k.Mailer, adapter: Bamboo.TestAdapter
|
config :shift73k, Shift73k.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
# Import secret config
|
# Import secret config
|
||||||
import_config "test.secret.exs"
|
# import_config "test.secret.exs"
|
||||||
|
|
7
entrypoint.sh
Executable file
7
entrypoint.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/ash
|
||||||
|
|
||||||
|
export MIX_ENV="prod"
|
||||||
|
|
||||||
|
cd /app
|
||||||
|
mix ecto.migrate
|
||||||
|
exec mix phx.server
|
|
@ -108,6 +108,13 @@ defmodule Shift73k.Accounts do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def register_user(attrs) do
|
def register_user(attrs) do
|
||||||
|
# If attrs has atom keys, convert to string
|
||||||
|
# If attrs don't include role, put default role
|
||||||
|
attrs =
|
||||||
|
attrs
|
||||||
|
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||||
|
|> Map.put_new("role", registration_role())
|
||||||
|
|
||||||
%User{}
|
%User{}
|
||||||
|> User.registration_changeset(attrs)
|
|> User.registration_changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
|
@ -20,19 +20,19 @@ defmodule Shift73k.Accounts.User do
|
||||||
@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 "users" do
|
schema "users" do
|
||||||
field(:email, :string)
|
field :email, :string
|
||||||
field(:password, :string, virtual: true)
|
field :password, :string, virtual: true
|
||||||
field(:hashed_password, :string)
|
field :hashed_password, :string
|
||||||
field(:confirmed_at, :naive_datetime)
|
field :confirmed_at, :naive_datetime
|
||||||
field(:calendar_slug, :string, default: Ecto.UUID.generate())
|
field :calendar_slug, :string, default: Ecto.UUID.generate()
|
||||||
|
|
||||||
field(:role, Ecto.Enum, values: Keyword.keys(@roles), default: :user)
|
field :role, Ecto.Enum, values: Keyword.keys(@roles), default: :user
|
||||||
field(:week_start_at, Ecto.Enum, values: weekdays(), default: :monday)
|
field :week_start_at, Ecto.Enum, values: weekdays(), default: :monday
|
||||||
|
|
||||||
has_many(:shift_templates, ShiftTemplate)
|
has_many :shift_templates, ShiftTemplate
|
||||||
belongs_to(:fave_shift_template, ShiftTemplate)
|
belongs_to :fave_shift_template, ShiftTemplate
|
||||||
|
|
||||||
has_many(:shifts, Shift)
|
has_many :shifts, Shift
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,19 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
alias Shift73k.Mailer
|
alias Shift73k.Mailer
|
||||||
alias Shift73k.Mailer.UserEmail
|
alias Shift73k.Mailer.UserEmail
|
||||||
|
|
||||||
|
def deliver(user_email, subject, body) do
|
||||||
|
%Swoosh.Email{} = email = UserEmail.compose(user_email, subject, body)
|
||||||
|
case Mailer.deliver(email) do
|
||||||
|
{:ok, msg} -> {:ok, msg, email}
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to confirm account.
|
Deliver instructions to confirm account.
|
||||||
"""
|
"""
|
||||||
def deliver_confirmation_instructions(user, url) do
|
def deliver_confirmation_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Confirmation instructions", """
|
||||||
|> UserEmail.compose("Confirm Your Account", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -21,15 +28,13 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to reset a user password.
|
Deliver instructions to reset a user password.
|
||||||
"""
|
"""
|
||||||
def deliver_reset_password_instructions(user, url) do
|
def deliver_reset_password_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Reset password instructions", """
|
||||||
|> UserEmail.compose("Reset Your Password", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -43,15 +48,13 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deliver instructions to update a user email.
|
Deliver instructions to update a user email.
|
||||||
"""
|
"""
|
||||||
def deliver_update_email_instructions(user, url) do
|
def deliver_update_email_instructions(user, url) do
|
||||||
user
|
deliver(user.email, "Update email instructions", """
|
||||||
|> UserEmail.compose("Change Your E-mail", """
|
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -65,6 +68,5 @@ defmodule Shift73k.Accounts.UserNotifier do
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
""")
|
""")
|
||||||
|> Mailer.deliver_later()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,10 +15,10 @@ defmodule Shift73k.Accounts.UserToken do
|
||||||
@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 "users_tokens" do
|
schema "users_tokens" do
|
||||||
field(:token, :binary)
|
field :token, :binary
|
||||||
field(:context, :string)
|
field :context, :string
|
||||||
field(:sent_to, :string)
|
field :sent_to, :string
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
|
|
||||||
timestamps(updated_at: false)
|
timestamps(updated_at: false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
defmodule Shift73k.Mailer do
|
defmodule Shift73k.Mailer do
|
||||||
use Bamboo.Mailer, otp_app: :shift73k
|
use Swoosh.Mailer, otp_app: :shift73k
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
defmodule Shift73k.Mailer.UserEmail do
|
defmodule Shift73k.Mailer.UserEmail do
|
||||||
import Bamboo.Email
|
import Swoosh.Email
|
||||||
|
|
||||||
@mailer_vars Application.compile_env(:shift73k, :app_global_vars,
|
@mailer_vars Application.compile_env(:shift73k, :app_global_vars,
|
||||||
mailer_reply_to: "admin@example.com",
|
mailer_reply_to: "admin@example.com",
|
||||||
mailer_from: {"Shift73k", "shift73k@example.com"}
|
mailer_from: {"Shift73k", "shift73k@example.com"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def compose(user, subject, body_text) do
|
def compose(user_email, subject, body_text) do
|
||||||
new_email()
|
new()
|
||||||
|> from(@mailer_vars[:mailer_from])
|
|> from(@mailer_vars[:mailer_from])
|
||||||
|> to(user.email)
|
|> to(user_email)
|
||||||
|> put_header("Reply-To", @mailer_vars[:mailer_reply_to])
|
|> header("Reply-To", @mailer_vars[:mailer_reply_to])
|
||||||
|> subject(subject)
|
|> subject(subject)
|
||||||
|> text_body(body_text)
|
|> text_body(body_text)
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,8 +86,12 @@ defmodule Shift73k.Shifts do
|
||||||
** (Ecto.NoResultsError)
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
def get_shift!(nil), do: nil
|
||||||
def get_shift!(id), do: Repo.get!(Shift, id)
|
def get_shift!(id), do: Repo.get!(Shift, id)
|
||||||
|
|
||||||
|
def get_shift(nil), do: nil
|
||||||
|
def get_shift(id), do: Repo.get(Shift, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a shift.
|
Creates a shift.
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Shift73k.Shifts.Shift do
|
||||||
field :time_start, :time
|
field :time_start, :time
|
||||||
field :time_end, :time
|
field :time_end, :time
|
||||||
|
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,8 +16,8 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
field :time_start, :time, default: ~T[09:00:00]
|
field :time_start, :time, default: ~T[09:00:00]
|
||||||
field :time_end, :time, default: ~T[17:00:00]
|
field :time_end, :time, default: ~T[17:00:00]
|
||||||
|
|
||||||
belongs_to(:user, Shift73k.Accounts.User)
|
belongs_to :user, Shift73k.Accounts.User
|
||||||
has_one(:is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id)
|
has_one :is_fave_of_user, Shift73k.Accounts.User, foreign_key: :fave_shift_template_id
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -57,6 +57,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|> validate_not_nil([:time_zone])
|
||||||
|> validate_inclusion(:time_zone, Tzdata.zone_list(),
|
|> validate_inclusion(:time_zone, Tzdata.zone_list(),
|
||||||
message: "must be a valid IANA tz database time zone"
|
message: "must be a valid IANA tz database time zone"
|
||||||
)
|
)
|
||||||
|
@ -72,4 +73,14 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
|
|> Map.drop([:__meta__, :id, :inserted_at, :updated_at, :user, :is_fave_of_user])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_not_nil(changeset, fields) do
|
||||||
|
Enum.reduce(fields, changeset, fn field, changeset ->
|
||||||
|
if get_field(changeset, field) == nil do
|
||||||
|
add_error(changeset, field, "nil")
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,9 +105,6 @@ defmodule Shift73kWeb do
|
||||||
# Import basic rendering functionality (render, render_layout, etc)
|
# Import basic rendering functionality (render, render_layout, etc)
|
||||||
import Phoenix.View
|
import Phoenix.View
|
||||||
|
|
||||||
# Import SVG Icon helper
|
|
||||||
import Shift73kWeb.IconHelpers
|
|
||||||
|
|
||||||
import Shift73kWeb.ErrorHelpers
|
import Shift73kWeb.ErrorHelpers
|
||||||
import Shift73kWeb.Gettext
|
import Shift73kWeb.Gettext
|
||||||
alias Shift73kWeb.Router.Helpers, as: Routes
|
alias Shift73kWeb.Router.Helpers, as: Routes
|
||||||
|
|
|
@ -10,12 +10,11 @@ defmodule Shift73kWeb.Endpoint do
|
||||||
signing_salt: "9CKxo0VJ"
|
signing_salt: "9CKxo0VJ"
|
||||||
]
|
]
|
||||||
|
|
||||||
socket("/socket", Shift73kWeb.UserSocket,
|
socket "/socket", Shift73kWeb.UserSocket,
|
||||||
websocket: true,
|
websocket: true,
|
||||||
longpoll: false
|
longpoll: false
|
||||||
)
|
|
||||||
|
|
||||||
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
|
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
#
|
#
|
||||||
|
@ -23,38 +22,54 @@ defmodule Shift73kWeb.Endpoint do
|
||||||
# when deploying your static files in production.
|
# when deploying your static files in production.
|
||||||
#
|
#
|
||||||
# file list generated by simple ls -1 assets/static/ - then copy/paste here
|
# file list generated by simple ls -1 assets/static/ - then copy/paste here
|
||||||
plug(Plug.Static,
|
plug Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :shift73k,
|
from: :shift73k,
|
||||||
gzip: (Mix.env() not in [:dev, :test]),
|
gzip: false,
|
||||||
only: "priv/static" |> Path.expand() |> File.ls!()
|
only: ~w(assets
|
||||||
)
|
android-chrome-192x192.png
|
||||||
|
android-chrome-512x512.png
|
||||||
|
apple-touch-icon.png
|
||||||
|
browserconfig.xml
|
||||||
|
favicon-16x16.png
|
||||||
|
favicon-32x32.png
|
||||||
|
favicon.ico
|
||||||
|
mstile-144x144.png
|
||||||
|
mstile-150x150.png
|
||||||
|
mstile-310x150.png
|
||||||
|
mstile-310x310.png
|
||||||
|
mstile-70x70.png
|
||||||
|
robots.txt
|
||||||
|
safari-pinned-tab.svg
|
||||||
|
site.webmanifest)
|
||||||
|
|
||||||
|
# For using vite.js in dev, we need to instruct Phoenix to serve files at assets/src over the usual endpoint. This is only necessary in development.
|
||||||
|
if Mix.env() == :dev do
|
||||||
|
plug Plug.Static,
|
||||||
|
at: "/",
|
||||||
|
from: "assets",
|
||||||
|
gzip: false
|
||||||
|
end
|
||||||
|
|
||||||
# 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.
|
||||||
if code_reloading? do
|
if code_reloading? do
|
||||||
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
|
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||||
plug(Phoenix.LiveReloader)
|
plug Phoenix.LiveReloader
|
||||||
plug(Phoenix.CodeReloader)
|
plug Phoenix.CodeReloader
|
||||||
plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k)
|
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :shift73k
|
||||||
end
|
end
|
||||||
|
|
||||||
plug(Phoenix.LiveDashboard.RequestLogger,
|
plug Plug.RequestId
|
||||||
param_key: "request_logger",
|
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||||
cookie_key: "request_logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
plug(Plug.RequestId)
|
plug Plug.Parsers,
|
||||||
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
|
|
||||||
|
|
||||||
plug(Plug.Parsers,
|
|
||||||
parsers: [:urlencoded, :multipart, :json],
|
parsers: [:urlencoded, :multipart, :json],
|
||||||
pass: ["*/*"],
|
pass: ["*/*"],
|
||||||
json_decoder: Phoenix.json_library()
|
json_decoder: Phoenix.json_library()
|
||||||
)
|
|
||||||
|
|
||||||
plug(Plug.MethodOverride)
|
plug Plug.MethodOverride
|
||||||
plug(Plug.Head)
|
plug Plug.Head
|
||||||
plug(Plug.Session, @session_options)
|
plug Plug.Session, @session_options
|
||||||
plug(Shift73kWeb.Router)
|
plug Shift73kWeb.Router
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule Shift73kWeb.LiveHelpers do
|
defmodule Shift73kWeb.LiveHelpers do
|
||||||
import Phoenix.LiveView
|
import Phoenix.LiveView
|
||||||
import Phoenix.LiveView.Helpers
|
|
||||||
|
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
@ -19,27 +18,6 @@ defmodule Shift73kWeb.LiveHelpers do
|
||||||
"""
|
"""
|
||||||
def live_okreply(socket), do: {:ok, socket}
|
def live_okreply(socket), do: {:ok, socket}
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a component inside the `Shift73kWeb.ModalComponent` component.
|
|
||||||
|
|
||||||
The rendered modal receives a `:return_to` option to properly update
|
|
||||||
the URL when the modal is closed.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<%= live_modal @socket, Shift73kWeb.PropertyLive.FormComponent,
|
|
||||||
id: @property.id || :new,
|
|
||||||
action: @live_action,
|
|
||||||
property: @property,
|
|
||||||
return_to: Routes.property_index_path(@socket, :index) %>
|
|
||||||
"""
|
|
||||||
def live_modal(socket, component, opts) do
|
|
||||||
modal_opts = [id: :modal, component: component, opts: opts]
|
|
||||||
# dirty little workaround for elixir complaining about socket being unused
|
|
||||||
_socket = socket
|
|
||||||
live_component(socket, Shift73kWeb.ModalComponent, modal_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Loads default assigns for liveviews
|
Loads default assigns for liveviews
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,12 +3,12 @@ defmodule Shift73kWeb.ModalComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~H"""
|
||||||
<div id="<%= @id %>" class="modal fade"
|
<div id={@id} class="modal fade"
|
||||||
phx-hook="BsModal"
|
phx-hook="BsModal"
|
||||||
phx-window-keydown="hide"
|
phx-window-keydown="hide"
|
||||||
phx-key="escape"
|
phx-key="escape"
|
||||||
phx-target="#<%= @id %>"
|
phx-target={"#" <> to_string(@id)}
|
||||||
phx-page-loading>
|
phx-page-loading>
|
||||||
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
@ -16,10 +16,10 @@ defmodule Shift73kWeb.ModalComponent do
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
|
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
|
||||||
<button type="button" class="btn-close" phx-click="hide" phx-target="<%= @myself %>" aria-label="Close"></button>
|
<button type="button" class="btn-close" phx-click="hide" phx-target={@myself} aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= live_component @socket, @component, Keyword.put(@opts, :modal_id, @id) %>
|
<%= live_component @component, Keyword.put(@opts, :modal_id, @id) %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<p>Are you sure you want to delete all assigned shifts from the selected days?</p>
|
||||||
|
|
||||||
|
<%= for {y, data} <- @date_map do %>
|
||||||
|
<dt><%= y %></dt>
|
||||||
|
<% months = Map.keys(data) %>
|
||||||
|
<dd>
|
||||||
|
<%= for {m, i} <- Enum.with_index(months, 1) do %>
|
||||||
|
<%= data |> Map.get(m) |> hd() |> Calendar.strftime("%b") %>:
|
||||||
|
<% days = Map.get(data, m) %>
|
||||||
|
<%= for {d, i} <- Enum.with_index(days, 1) do %>
|
||||||
|
<%= d.day %><%= if i < length(days) do %>,<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if i < length(months) do %><br /><% end %>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm-delete-days-shifts",
|
||||||
|
phx_target: @myself
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,31 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<p>Are you sure you want to delete all assigned shifts from the selected days?</p>
|
|
||||||
|
|
||||||
<%= for {y, data} <- @date_map do %>
|
|
||||||
<dt><%= y %></dt>
|
|
||||||
<% months = Map.keys(data) %>
|
|
||||||
<dd>
|
|
||||||
<%= for {m, i} <- Enum.with_index(months, 1) do %>
|
|
||||||
<%= data |> Map.get(m) |> hd() |> Calendar.strftime("%b") %>:
|
|
||||||
<% days = Map.get(data, m) %>
|
|
||||||
<%= for {d, i} <- Enum.with_index(days, 1) do %>
|
|
||||||
<%= d.day %><%= if i < length(days) do %>,<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= if i < length(months) do %><br /><% end %>
|
|
||||||
<% end %>
|
|
||||||
</dd>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm-delete-days-shifts",
|
|
||||||
phx_target: @myself
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Shift73kWeb.ShiftAssignLive.Index do
|
defmodule Shift73kWeb.ShiftAssignLive.Index do
|
||||||
use Shift73kWeb, :live_view
|
use Shift73kWeb, :live_view
|
||||||
|
import Shift73k, only: [app_time_zone: 0]
|
||||||
|
|
||||||
alias Shift73k.Repo
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Shifts
|
alias Shift73k.Shifts
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
<%= if @delete_days_shifts do %>
|
<%= if @delete_days_shifts do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftAssignLive.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftAssignLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: "delete-days-shifts-#{@current_user.id}",
|
id: "delete-days-shifts-#{@current_user.id}",
|
||||||
title: "Delete Shifts From Selected Days",
|
title: "Delete Shifts From Selected Days",
|
||||||
delete_days_shifts: @delete_days_shifts,
|
delete_days_shifts: @delete_days_shifts,
|
||||||
current_user: @current_user
|
current_user: @current_user
|
||||||
%>
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-calendar2-plus", [class: "icon baseline"] %>
|
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||||
Schedule Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="row justify-content-center mt-4">
|
<div class="row justify-content-center mt-4">
|
||||||
|
@ -24,14 +28,19 @@
|
||||||
%>
|
%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<button type="button" class="ms-2 btn btn-primary text-nowrap <%= if @show_template_btn_active, do: "active" %>" id="#templateDetailsBtn" phx-click="toggle-template-details" phx-value-target_id="#templateDetailsCol">
|
<% details_button_class = "ms-2 btn btn-primary text-nowrap"
|
||||||
<%= icon_div @socket, (@show_template_btn_active && "bi-binoculars-fill" || "bi-binoculars"), [class: "icon baseline"] %>
|
details_button_class = if @show_template_btn_active, do: "#{details_button_class} active", else: details_button_class %>
|
||||||
|
<button type="button" class={details_button_class} id="#templateDetailsBtn" phx-click="toggle-template-details" phx-value-target_id="#templateDetailsCol">
|
||||||
|
<i class={@show_template_btn_active && "bi bi-binoculars-fill me-sm-1" || "bi bi-binoculars me-sm-1"}></i>
|
||||||
<span class="d-none d-sm-inline">Details</span>
|
<span class="d-none d-sm-inline">Details</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-9 col-xl-8 col-xxl-7 <%= @show_template_details && "collapse show" || "collapse" %>" id="#templateDetailsCol" phx-hook="BsCollapse">
|
|
||||||
|
<% template_details_div_class = "col-12 col-lg-9 col-xl-8 col-xxl-7 collapse"
|
||||||
|
template_details_div_class = if @show_template_details, do: "#{template_details_div_class} show", else: template_details_div_class %>
|
||||||
|
<div class={template_details_div_class} id="#templateDetailsCol" phx-hook="BsCollapse">
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
@ -41,8 +50,8 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :subject, "Subject/Title", class: "form-label" %>
|
<%= label stf, :subject, "Subject/Title", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :subject) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :subject)}>
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-tag icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :subject,
|
<%= text_input stf, :subject,
|
||||||
value: input_value(stf, :subject),
|
value: input_value(stf, :subject),
|
||||||
class: input_class(stf, :subject, "form-control"),
|
class: input_class(stf, :subject, "form-control"),
|
||||||
|
@ -57,25 +66,27 @@
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
<div class="row gx-2 gx-sm-3">
|
<div class="row gx-2 gx-sm-3">
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(stf, :time_start) %>">
|
<div class="col-6" phx-feedback-for={input_id(stf, :time_start)}>
|
||||||
<%= label stf, :time_start, "Start", class: "form-label" %>
|
<%= label stf, :time_start, "Start", class: "form-label" %>
|
||||||
<%= time_input stf, :time_start,
|
<%= time_input stf, :time_start,
|
||||||
precision: :minute,
|
precision: :minute,
|
||||||
value: input_value(stf, :time_start),
|
value: input_value(stf, :time_start),
|
||||||
class: input_class(stf, :time_start, "form-control"),
|
class: input_class(stf, :time_start, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
aria_describedby: error_ids(stf, :time_start)
|
aria_describedby: error_ids(stf, :time_start),
|
||||||
|
required: true
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(stf, :time_end) %>">
|
<div class="col-6" phx-feedback-for={input_id(stf, :time_end)}>
|
||||||
<%= label stf, :time_end, "End", class: "form-label" %>
|
<%= label stf, :time_end, "End", class: "form-label" %>
|
||||||
<%= time_input stf, :time_end,
|
<%= time_input stf, :time_end,
|
||||||
precision: :minute,
|
precision: :minute,
|
||||||
value: input_value(stf, :time_end),
|
value: input_value(stf, :time_end),
|
||||||
class: input_class(stf, :time_end, "form-control"),
|
class: input_class(stf, :time_end, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
aria_describedby: error_ids(stf, :time_end)
|
aria_describedby: error_ids(stf, :time_end),
|
||||||
|
required: true
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -83,18 +94,18 @@
|
||||||
|
|
||||||
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
||||||
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_start) %>">
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(stf, :time_start)}>
|
||||||
<%= error_tag stf, :time_start %>
|
<%= error_tag stf, :time_start %>
|
||||||
</div>
|
</div>
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(stf, :time_end) %>">
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(stf, :time_end)}>
|
||||||
<%= error_tag stf, :time_end %>
|
<%= error_tag stf, :time_end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :location, class: "form-label" %>
|
<%= label stf, :location, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(stf, :location) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(stf, :location)}>
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-geo icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :location,
|
<%= text_input stf, :location,
|
||||||
value: input_value(stf, :location),
|
value: input_value(stf, :location),
|
||||||
class: input_class(stf, :location, "form-control"),
|
class: input_class(stf, :location, "form-control"),
|
||||||
|
@ -108,18 +119,19 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<%= label stf, :time_zone, class: "form-label" %>
|
<%= label stf, :time_zone, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3 mb-md-0" phx-feedback-for="<%= input_id(stf, :time_zone) %>">
|
<div class="inner-addon left-addon mb-3 mb-md-0" phx-feedback-for={input_id(stf, :time_zone)}>
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input stf, :time_zone,
|
<%= text_input stf, :time_zone,
|
||||||
value: input_value(stf, :time_zone),
|
value: input_value(stf, :time_zone),
|
||||||
class: input_class(stf, :time_zone, "form-control"),
|
class: input_class(stf, :time_zone, "form-control"),
|
||||||
disabled: @shift_template.id != @custom_shift.id,
|
disabled: @shift_template.id != @custom_shift.id,
|
||||||
phx_debounce: 250,
|
phx_debounce: 250,
|
||||||
list: "tz_list"
|
list: "tz_list",
|
||||||
|
placeholder: "Default: #{app_time_zone()}"
|
||||||
%>
|
%>
|
||||||
<datalist id="tz_list">
|
<datalist id="tz_list">
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
<option value="<%= tz_name %>"></option>
|
<option value={tz_name}></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
end
|
end
|
||||||
</datalist>
|
</datalist>
|
||||||
|
@ -132,7 +144,7 @@
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<%= label stf, :description, class: "form-label" %>
|
<%= label stf, :description, class: "form-label" %>
|
||||||
<div phx-feedback-for="<%= input_id(stf, :description) %>">
|
<div phx-feedback-for={input_id(stf, :description)}>
|
||||||
|
|
||||||
<%= textarea stf, :description,
|
<%= textarea stf, :description,
|
||||||
value: input_value(stf, :description),
|
value: input_value(stf, :description),
|
||||||
|
@ -174,17 +186,17 @@
|
||||||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
|
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" disabled={if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
<i class="bi bi-asterisk me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Today</span>
|
<span class="d-none d-sm-inline">Today</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Prev</span>
|
<span class="d-none d-sm-inline">Prev</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||||
<span class="d-none d-sm-inline">Next</span>
|
<span class="d-none d-sm-inline">Next</span>
|
||||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -204,7 +216,7 @@
|
||||||
<%= for week <- @week_rows do %>
|
<%= for week <- @week_rows do %>
|
||||||
<tr>
|
<tr>
|
||||||
<%= for day <- week do %>
|
<%= for day <- week do %>
|
||||||
<td class="<%= day_color(day, @current_date, @cursor_date, @selected_days) %>" phx-click="select-day" phx-value-day="<%= day %>">
|
<td class={day_color(day, @current_date, @cursor_date, @selected_days)} phx-click="select-day" phx-value-day={day}>
|
||||||
|
|
||||||
<%= Calendar.strftime(day, "%d") %><%= if day.month != @cursor_date.month, do: "-#{Calendar.strftime(day, "%b")}" %>
|
<%= Calendar.strftime(day, "%d") %><%= if day.month != @cursor_date.month, do: "-#{Calendar.strftime(day, "%b")}" %>
|
||||||
|
|
||||||
|
@ -236,19 +248,16 @@
|
||||||
<div class="row justify-content-center justify-content-lg-end my-5">
|
<div class="row justify-content-center justify-content-lg-end my-5">
|
||||||
<div class="col-12 col-sm-10 col-md-8 col-lg-auto d-flex flex-column-reverse flex-lg-row">
|
<div class="col-12 col-sm-10 col-md-8 col-lg-auto d-flex flex-column-reverse flex-lg-row">
|
||||||
|
|
||||||
<button class="btn btn-outline-danger mb-1 mb-lg-0 me-lg-1" phx-click="delete-days-shifts" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
|
<button class="btn btn-outline-danger mb-1 mb-lg-0 me-lg-1" phx-click="delete-days-shifts" disabled={if Enum.empty?(@selected_days), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete shifts from selected days
|
||||||
Delete shifts from selected days
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-outline-dark mb-1 mb-lg-0 me-lg-1" phx-click="clear-days" <%= if Enum.empty?(@selected_days), do: "disabled" %>>
|
<button class="btn btn-outline-dark mb-1 mb-lg-0 me-lg-1" phx-click="clear-days" disabled={if Enum.empty?(@selected_days), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-eraser", [class: "icon baseline"] %>
|
<i class="bi bi-eraser me-1"></i> De-select all selected
|
||||||
De-select all selected
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-primary mb-1 mb-lg-0" phx-click="save-days" <%= if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: "disabled" %>>
|
<button class="btn btn-primary mb-1 mb-lg-0" phx-click="save-days" disabled={if (!@shift_template_changeset.valid? || Enum.empty?(@selected_days)), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-save", [class: "icon baseline"] %>
|
<i class="bi bi-save me-1"></i> Assign shifts to selected days
|
||||||
Assign shifts to selected days
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
|
@ -2,10 +2,8 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @socket, "bi-box-arrow-in-left", [class: "icon baseline"] %>
|
<i class="bi bi-box-arrow-in-left me-1"></i> Import Shifts
|
||||||
Import Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="lead">If you have an iCal/ics formatted calendar hosted elsewhere, provide its URL here to import its events.</p>
|
<p class="lead">If you have an iCal/ics formatted calendar hosted elsewhere, provide its URL here to import its events.</p>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
@ -17,7 +15,7 @@
|
||||||
<% valid_class = @url_validated && "is-valid" || "" %>
|
<% valid_class = @url_validated && "is-valid" || "" %>
|
||||||
<%= label iimf, :ics_url, "iCal/ics URL", class: "form-label" %>
|
<%= label iimf, :ics_url, "iCal/ics URL", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-link", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-link icon is-left text-muted fs-5"></i>
|
||||||
<%= url_input iimf, :ics_url,
|
<%= url_input iimf, :ics_url,
|
||||||
class: show_url_error && "form-control is-invalid" || "form-control #{valid_class}",
|
class: show_url_error && "form-control is-invalid" || "form-control #{valid_class}",
|
||||||
phx_debounce: 500,
|
phx_debounce: 500,
|
||||||
|
@ -33,7 +31,7 @@
|
||||||
|
|
||||||
<%= label iimf, :time_zone, class: "form-label" %>
|
<%= label iimf, :time_zone, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
<%= text_input iimf, :time_zone,
|
<%= text_input iimf, :time_zone,
|
||||||
value: Shift73k.app_time_zone(),
|
value: Shift73k.app_time_zone(),
|
||||||
class: @tz_valid && "form-control" || "form-control is-invalid",
|
class: @tz_valid && "form-control" || "form-control is-invalid",
|
||||||
|
@ -43,7 +41,7 @@
|
||||||
%>
|
%>
|
||||||
<datalist id="tz_list">
|
<datalist id="tz_list">
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
<option value="<%= tz_name %>"></option>
|
<option value={tz_name}></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</datalist>
|
</datalist>
|
||||||
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
46
lib/shift73k_web/live/shift_live/delete_component.ex
Normal file
46
lib/shift73k_web/live/shift_live/delete_component.ex
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule Shift73kWeb.ShiftLive.DeleteComponent do
|
||||||
|
use Shift73kWeb, :live_component
|
||||||
|
|
||||||
|
alias Shift73k.Shifts
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> live_okreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("confirm", %{"id" => id, "subject" => subject, "datetime" => datetime}, socket) do
|
||||||
|
shift = Shifts.get_shift(id)
|
||||||
|
|
||||||
|
if (shift) do
|
||||||
|
shift
|
||||||
|
|> Shifts.delete_shift()
|
||||||
|
|> case do
|
||||||
|
{:ok, _} ->
|
||||||
|
flash = {:info, "Shift deleted successfully: \"#{subject}\""}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
handle_error(socket, subject, datetime)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_error(socket, subject, datetime) do
|
||||||
|
flash =
|
||||||
|
{:error,
|
||||||
|
"Some error trying to delete shift \"#{subject} (#{datetime})\". Possibly already deleted? Reloading list..."}
|
||||||
|
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
end
|
22
lib/shift73k_web/live/shift_live/delete_component.html.heex
Normal file
22
lib/shift73k_web/live/shift_live/delete_component.html.heex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<% shift_datetime = "#{Calendar.strftime(@delete_shift.date, "%A, %b %-d")}, #{format_shift_time(@delete_shift.time_start)} — #{format_shift_time(@delete_shift.time_end)}" %>
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_shift.subject %> (<%= shift_datetime %>)?"
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_shift.id,
|
||||||
|
phx_value_subject: @delete_shift.subject,
|
||||||
|
phx_value_datetime: shift_datetime %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -22,6 +22,7 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
socket
|
socket
|
||||||
|> init_today(Date.utc_today())
|
|> init_today(Date.utc_today())
|
||||||
|> update_agenda()
|
|> update_agenda()
|
||||||
|
|> assign_modal_close_handlers()
|
||||||
|> assign(:delete_shift, nil)
|
|> assign(:delete_shift, nil)
|
||||||
|> apply_action(socket.assigns.live_action, params)
|
|> apply_action(socket.assigns.live_action, params)
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
@ -33,6 +34,11 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assign_modal_close_handlers(socket) do
|
||||||
|
to = Routes.shift_index_path(socket, :index)
|
||||||
|
assign(socket, modal_return_to: to, modal_close_action: :return)
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :index, _params) do
|
defp apply_action(socket, :index, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "My Shifts")
|
|> assign(:page_title, "My Shifts")
|
||||||
|
@ -76,6 +82,14 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
|> assign_known_shifts()
|
|> assign_known_shifts()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("delete-modal", %{"id" => id}, socket) do
|
||||||
|
socket
|
||||||
|
|> assign(:modal_close_action, :delete_shift)
|
||||||
|
|> assign(:delete_shift, Shifts.get_shift!(id))
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
shift = Shifts.get_shift!(id)
|
shift = Shifts.get_shift!(id)
|
||||||
|
@ -94,6 +108,28 @@ defmodule Shift73kWeb.ShiftLive.Index do
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
|
||||||
|
socket
|
||||||
|
|> copy_flash()
|
||||||
|
|> push_patch(to: socket.assigns.modal_return_to)
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
|
||||||
|
socket
|
||||||
|
|> assign(assign_key, nil)
|
||||||
|
|> assign_modal_close_handlers()
|
||||||
|
|> assign_known_shifts()
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||||
|
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
defp new_nav_cursor("now", _cursor_date), do: Date.utc_today()
|
defp new_nav_cursor("now", _cursor_date), do: Date.utc_today()
|
||||||
|
|
||||||
defp new_nav_cursor(nav, cursor_date) do
|
defp new_nav_cursor(nav, cursor_date) do
|
||||||
|
|
|
@ -1,40 +1,48 @@
|
||||||
|
<%= if @delete_shift do %>
|
||||||
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
|
id: @delete_shift.id,
|
||||||
|
title: "Delete Shift Template",
|
||||||
|
delete_shift: @delete_shift
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<div class="row justify-content-start justify-content-sm-center">
|
<div class="row justify-content-start justify-content-sm-center">
|
||||||
<div class="col-md-10 col-xl-10">
|
<div class="col-md-10 col-xl-10">
|
||||||
|
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-card-list", [class: "icon baseline"] %>
|
<i class="bi bi-card-list me-1"></i> My Shifts
|
||||||
My Shifts
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row justify-content-start justify-content-sm-center">
|
<div class="row justify-content-start justify-content-sm-center">
|
||||||
<div class="col-md-10 col-xl-10">
|
<div class="col-md-10 col-xl-10">
|
||||||
|
|
||||||
|
|
||||||
<%# month navigation %>
|
<%# month navigation %>
|
||||||
<div class="d-flex justify-content-between align-items-end my-4">
|
<div class="d-flex justify-content-between align-items-end my-4">
|
||||||
<h3 class="text-muted mb-0">
|
<h3 class="text-muted mb-0">
|
||||||
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
<%= Calendar.strftime(@cursor_date, "%B %Y") %>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
|
<button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" disabled={if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: :true, else: :false}>
|
||||||
<%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
|
<i class="bi bi-asterisk me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Today</span>
|
<span class="d-none d-sm-inline">Today</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
|
||||||
<%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
<i class="bi bi-chevron-left me-sm-1"></i>
|
||||||
<span class="d-none d-sm-inline">Prev</span>
|
<span class="d-none d-sm-inline">Prev</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
<button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
|
||||||
<span class="d-none d-sm-inline">Next</span>
|
<span class="d-none d-sm-inline">Next</span>
|
||||||
<%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
|
<i class="bi bi-chevron-right ms-sm-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<%= for day <- Enum.to_list(@date_range) do %>
|
<%= for day <- Enum.to_list(@date_range) do %>
|
||||||
<%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
|
<%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
|
||||||
<div class="border-top mt-4 mb-4"></div>
|
<div class="border-top mt-4 mb-4"></div>
|
||||||
|
@ -45,24 +53,21 @@
|
||||||
<% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
|
<% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
|
||||||
<%= if !Enum.empty?(day_shifts) do %>
|
<%= if !Enum.empty?(day_shifts) do %>
|
||||||
|
|
||||||
|
|
||||||
<%= for shift <- day_shifts do %>
|
<%= for shift <- day_shifts do %>
|
||||||
|
|
||||||
|
<div class="card mt-2 mb-4 col-12 ms-sm-3 ms-md-4 col-lg-10 ms-lg-5 col-xxl-8" id={"shift-#{shift.id}"}>
|
||||||
<div class="card mt-2 mb-4 col-12 ms-sm-3 ms-md-4 col-lg-10 ms-lg-5 col-xxl-8" id="shift-<%= shift.id %>">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline text-muted me-1"] %>
|
<i class="bi bi-tag text-muted me-1"></i>
|
||||||
<%= shift.subject %>
|
<%= shift.subject %>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
|
||||||
<table class="table table-borderless table-nonfluid table-sm">
|
<table class="table table-borderless table-nonfluid table-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-hourglass text-muted"></i>
|
||||||
<span class="visually-hidden">Hours:</span>
|
<span class="visually-hidden">Hours:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -79,7 +84,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-geo text-muted"></i>
|
||||||
<span class="visually-hidden">Location:</span>
|
<span class="visually-hidden">Location:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -92,7 +97,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-justify-left text-muted"></i>
|
||||||
<span class="visually-hidden">Description:</span>
|
<span class="visually-hidden">Description:</span>
|
||||||
</th>
|
</th>
|
||||||
<td class="shift-description">
|
<td class="shift-description">
|
||||||
|
@ -106,51 +111,22 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<%#= if Roles.can?(@current_user, template, :edit) do %>
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={shift.id}>
|
||||||
<%#= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
<%#= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
</button>
|
||||||
<%# Edit %>
|
|
||||||
<%# end %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%#= if Roles.can?(@current_user, template, :delete) do %>
|
|
||||||
<%# <button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id=" %>
|
|
||||||
<%#= shift.id %>
|
|
||||||
<%# "> %>
|
|
||||||
<%#= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
|
||||||
<%# Delete %>
|
|
||||||
<%# </button> %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%= button to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"], class: "btn btn-outline-danger btn-sm text-nowrap" do %>
|
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
|
||||||
Delete
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="text-muted"><em>Nothing scheduled</em></p>
|
<p class="text-muted"><em>Nothing scheduled</em></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_shift_template.subject %>
|
||||||
|
(<%= format_shift_time(@delete_shift_template.time_start) %>
|
||||||
|
—
|
||||||
|
<%= format_shift_time(@delete_shift_template.time_end) %>)"?
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_shift_template.id,
|
||||||
|
phx_value_subject: @delete_shift_template.subject %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,19 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
Are you sure you want to delete "<%= @delete_shift_template.subject %>
|
|
||||||
(<%= format_shift_time(@delete_shift_template.time_start) %>
|
|
||||||
—
|
|
||||||
<%= format_shift_time(@delete_shift_template.time_end) %>)"?
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_value_id: @delete_shift_template.id,
|
|
||||||
phx_value_subject: @delete_shift_template.subject %>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
|
defmodule Shift73kWeb.ShiftTemplateLive.FormComponent do
|
||||||
use Shift73kWeb, :live_component
|
use Shift73kWeb, :live_component
|
||||||
|
import Shift73k, only: [app_time_zone: 0]
|
||||||
|
|
||||||
alias Shift73k.Shifts.Templates
|
alias Shift73k.Shifts.Templates
|
||||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" phx-target={@myself} id="shift_template-form">
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<%= label f, :subject, "Subject/Title", class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :subject)}>
|
||||||
|
<i class="bi bi-tag icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :subject,
|
||||||
|
value: input_value(f, :subject),
|
||||||
|
class: input_class(f, :subject, "form-control"),
|
||||||
|
autofocus: true,
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :subject)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :subject %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row gx-2 gx-sm-3">
|
||||||
|
|
||||||
|
<div class="col-6" phx-feedback-for={input_id(f, :time_start)}>
|
||||||
|
<%= label f, :time_start, "Start", class: "form-label" %>
|
||||||
|
<%= time_input f, :time_start,
|
||||||
|
precision: :minute,
|
||||||
|
value: input_value(f, :time_start),
|
||||||
|
class: input_class(f, :time_start, "form-control"),
|
||||||
|
aria_describedby: error_ids(f, :time_start),
|
||||||
|
required: true
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6" phx-feedback-for={input_id(f, :time_end)}>
|
||||||
|
<%= label f, :time_end, "End", class: "form-label" %>
|
||||||
|
<%= time_input f, :time_end,
|
||||||
|
precision: :minute,
|
||||||
|
value: input_value(f, :time_end),
|
||||||
|
class: input_class(f, :time_end, "form-control"),
|
||||||
|
aria_describedby: error_ids(f, :time_end),
|
||||||
|
required: true
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
||||||
|
|
||||||
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(f, :time_start)}>
|
||||||
|
<%= error_tag f, :time_start %>
|
||||||
|
</div>
|
||||||
|
<div class="phx-orphaned-feedback" phx-feedback-for={input_id(f, :time_end)}>
|
||||||
|
<%= error_tag f, :time_end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :location, class: "form-label mt-3" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :location)}>
|
||||||
|
<i class="bi bi-geo icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :location,
|
||||||
|
value: input_value(f, :location),
|
||||||
|
class: input_class(f, :location, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :location)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :location %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :description, class: "form-label" %>
|
||||||
|
<div class="mb-3" phx-feedback-for={input_id(f, :description)}>
|
||||||
|
|
||||||
|
<%= textarea f, :description,
|
||||||
|
value: input_value(f, :description),
|
||||||
|
class: input_class(f, :description, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
aria_describedby: error_ids(f, :description)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :description %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= label f, :time_zone, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :time_zone)}>
|
||||||
|
<i class="bi bi-map icon is-left text-muted fs-5"></i>
|
||||||
|
<%= text_input f, :time_zone,
|
||||||
|
value: input_value(f, :time_zone),
|
||||||
|
class: input_class(f, :time_zone, "form-control"),
|
||||||
|
phx_debounce: 250,
|
||||||
|
list: "tz_list",
|
||||||
|
placeholder: "Default: #{app_time_zone()}"
|
||||||
|
%>
|
||||||
|
<datalist id="tz_list">
|
||||||
|
<%= for tz_name <- Tzdata.zone_list() do %>
|
||||||
|
<option value={tz_name}></option>
|
||||||
|
<% end %>
|
||||||
|
</datalist>
|
||||||
|
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
||||||
|
<%= error_tag f, :time_zone %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= submit "Save",
|
||||||
|
class: "btn btn-primary ",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,116 +0,0 @@
|
||||||
<%= f = form_for @changeset, "#",
|
|
||||||
id: "shift_template-form",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_change: "validate",
|
|
||||||
phx_submit: "save" %>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<%= label f, :subject, "Subject/Title", class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :subject) %>">
|
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :subject,
|
|
||||||
value: input_value(f, :subject),
|
|
||||||
class: input_class(f, :subject, "form-control"),
|
|
||||||
autofocus: true,
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :subject)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :subject %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row gx-2 gx-sm-3">
|
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_start) %>">
|
|
||||||
<%= label f, :time_start, "Start", class: "form-label" %>
|
|
||||||
<%= time_input f, :time_start,
|
|
||||||
precision: :minute,
|
|
||||||
value: input_value(f, :time_start),
|
|
||||||
class: input_class(f, :time_start, "form-control"),
|
|
||||||
aria_describedby: error_ids(f, :time_start)
|
|
||||||
%>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6" phx-feedback-for="<%= input_id(f, :time_end) %>">
|
|
||||||
<%= label f, :time_end, "End", class: "form-label" %>
|
|
||||||
<%= time_input f, :time_end,
|
|
||||||
precision: :minute,
|
|
||||||
value: input_value(f, :time_end),
|
|
||||||
class: input_class(f, :time_end, "form-control"),
|
|
||||||
aria_describedby: error_ids(f, :time_end)
|
|
||||||
%>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="valid-feedback d-block text-primary">Shift length: <%= @shift_length %></div>
|
|
||||||
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_start) %>">
|
|
||||||
<%= error_tag f, :time_start %>
|
|
||||||
</div>
|
|
||||||
<div class="phx-orphaned-feedback" phx-feedback-for="<%= input_id(f, :time_end) %>">
|
|
||||||
<%= error_tag f, :time_end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :location, class: "form-label mt-3" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :location) %>">
|
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :location,
|
|
||||||
value: input_value(f, :location),
|
|
||||||
class: input_class(f, :location, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :location)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :location %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :description, class: "form-label" %>
|
|
||||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :description) %>">
|
|
||||||
|
|
||||||
<%= textarea f, :description,
|
|
||||||
value: input_value(f, :description),
|
|
||||||
class: input_class(f, :description, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
aria_describedby: error_ids(f, :description)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :description %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%= label f, :time_zone, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :time_zone) %>">
|
|
||||||
<%= icon_div @socket, "bi-map", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= text_input f, :time_zone,
|
|
||||||
value: input_value(f, :time_zone),
|
|
||||||
class: input_class(f, :time_zone, "form-control"),
|
|
||||||
phx_debounce: 250,
|
|
||||||
list: "tz_list"
|
|
||||||
%>
|
|
||||||
<datalist id="tz_list">
|
|
||||||
<%= for tz_name <- Tzdata.zone_list() do %>
|
|
||||||
<option value="<%= tz_name %>"></option>
|
|
||||||
<% end %>
|
|
||||||
</datalist>
|
|
||||||
<div class="valid-feedback d-block text-primary">Type to search & select from list of known <%= link "IANA tz database", to: "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", target: "_blank" %> time zones</div>
|
|
||||||
<%= error_tag f, :time_zone %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= submit "Save",
|
|
||||||
class: "btn btn-primary ",
|
|
||||||
disabled: !@changeset.valid?,
|
|
||||||
aria_disabled: !@changeset.valid? && "true" || false,
|
|
||||||
phx_disable_with: "Saving..."
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
|
@ -1,17 +1,29 @@
|
||||||
<%= if @live_action in [:new, :edit, :clone] do %>
|
<%= if @live_action in [:new, :edit, :clone] do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.FormComponent,
|
<.live_component
|
||||||
id: @shift_template.id || :new,
|
module={Shift73kWeb.ModalComponent}
|
||||||
title: @page_title,
|
id="modal"
|
||||||
action: @live_action,
|
component={Shift73kWeb.ShiftTemplateLive.FormComponent}
|
||||||
shift_template: @shift_template,
|
opts={[
|
||||||
current_user: @current_user %>
|
id: @shift_template.id || :new,
|
||||||
|
title: @page_title,
|
||||||
|
action: @live_action,
|
||||||
|
shift_template: @shift_template,
|
||||||
|
current_user: @current_user
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @delete_shift_template do %>
|
<%= if @delete_shift_template do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.ShiftTemplateLive.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: @delete_shift_template.id,
|
id: @delete_shift_template.id,
|
||||||
title: "Delete Shift Template",
|
title: "Delete Shift Template",
|
||||||
delete_shift_template: @delete_shift_template %>
|
delete_shift_template: @delete_shift_template
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,12 +32,10 @@
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center">
|
||||||
<h2 class="mb-3 mb-sm-0">
|
<h2 class="mb-3 mb-sm-0">
|
||||||
<%= icon_div @socket, "bi-clock-history", [class: "icon baseline"] %>
|
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||||
My Shift Templates
|
|
||||||
</h2>
|
</h2>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :new), class: "btn btn-primary" do %>
|
||||||
<%= icon_div @socket, "bi-plus-circle-dotted", [class: "icon baseline"] %>
|
<i class="bi bi-plus-circle-dotted me-1"></i> New Shift Template
|
||||||
New Shift Template
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -39,13 +49,13 @@
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<h5 class="card-header d-flex justify-content-between align-items-center">
|
<h5 class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span class="visually-hidden">Subject:</span>
|
<span class="visually-hidden">Subject:</span>
|
||||||
<%= icon_div @socket, "bi-tag", [class: "icon baseline me-1"] %>
|
<i class="bi bi-tag me-1"></i>
|
||||||
<div class="w-100"><%= template.subject %></div>
|
<div class="w-100"><%= template.subject %></div>
|
||||||
<%= if template.id == @current_user.fave_shift_template_id do %>
|
<% fav_icon_data = if template.id == @current_user.fave_shift_template_id, do:
|
||||||
<%= icon_div @socket, "bi-star-fill", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Unset as favorite", phx_click: "unset-user-fave-shift-template", class: "cursor-pointer"] %>
|
{"bi-star-fill", "Unset as favorite", "unset-user-fave-shift-template"}, else:
|
||||||
<% else %>
|
{"bi-star", "Set as favorite", "set-user-fave-shift-template"}
|
||||||
<%= icon_div @socket, "bi-star", [class: "icon baseline text-primary align-self-start ms-2"], [role: "img", aria_hidden: false, aria_label: "Set as favorite", phx_click: "set-user-fave-shift-template", phx_value_id: template.id, class: "cursor-pointer"] %>
|
%>
|
||||||
<% end %>
|
<i class={"bi #{elem(fav_icon_data, 0)} text-primary align-self-start ms-2 cursor-pointer"} role="img" aria-hidden="false" aria-label={elem(fav_icon_data, 1)} phx-click={elem(fav_icon_data, 2)} phx-value-id={template.id}></i>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
@ -53,7 +63,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-hourglass", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-hourglass text-muted"></i>
|
||||||
<span class="visually-hidden">Hours:</span>
|
<span class="visually-hidden">Hours:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -70,7 +80,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-geo", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-geo text-muted"></i>
|
||||||
<span class="visually-hidden">Location:</span>
|
<span class="visually-hidden">Location:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -83,7 +93,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="text-end">
|
<th scope="row" class="text-end">
|
||||||
<%= icon_div @socket, "bi-justify-left", [class: "icon baseline text-muted"] %>
|
<i class="bi bi-justify-left text-muted"></i>
|
||||||
<span class="visually-hidden">Description:</span>
|
<span class="visually-hidden">Description:</span>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -99,24 +109,19 @@
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :edit) do %>
|
<%= if Roles.can?(@current_user, template, :edit) do %>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :edit, template), class: "btn btn-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :clone) do %>
|
<%= if Roles.can?(@current_user, template, :clone) do %>
|
||||||
<%= live_patch to: Routes.shift_template_index_path(@socket, :clone, template), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.shift_template_index_path(@socket, :clone, template), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-clipboard-plus", [class: "icon baseline"] %>
|
<i class="bi bi-clipboard-plus me-1"></i> Clone
|
||||||
Clone
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%#= button "" %>
|
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, template, :delete) do %>
|
<%= if Roles.can?(@current_user, template, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= template.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={template.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Shift73kWeb.UserLive.Registration do
|
defmodule Shift73kWeb.UserLive.Registration do
|
||||||
use Shift73kWeb, :live_view
|
use Shift73kWeb, :live_view
|
||||||
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ defmodule Shift73kWeb.UserLive.Registration do
|
||||||
user_id: nil,
|
user_id: nil,
|
||||||
user_return_to: Map.get(session, "user_return_to", "/"),
|
user_return_to: Map.get(session, "user_return_to", "/"),
|
||||||
messages: [
|
messages: [
|
||||||
success: "Welcome! Your new account has been created, and you've been logged in.",
|
success: "Welcome! Your new account has been created, and you've been logged in."
|
||||||
info:
|
|
||||||
"Some features may be unavailable until you confirm your email address. Check your inbox for instructions."
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -35,19 +33,33 @@ defmodule Shift73kWeb.UserLive.Registration do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"user" => user_params}, socket) do
|
def handle_event("save", %{"user" => user_params}, socket) do
|
||||||
|
is_first_user = !Repo.exists?(User)
|
||||||
user_params
|
user_params
|
||||||
|> Map.put("role", Accounts.registration_role())
|
|
||||||
|> Accounts.register_user()
|
|> Accounts.register_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
{:ok, %Bamboo.Email{}} =
|
# If this is the first user, we just confirm them
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
if is_first_user do
|
||||||
user,
|
user |> User.confirm_changeset() |> Repo.update()
|
||||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
else
|
||||||
)
|
# Otherwise, all new users require email confirmation so we wend instructions
|
||||||
|
{:ok, _, %Swoosh.Email{} = _captured_email} =
|
||||||
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
|
user,
|
||||||
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
login_params =
|
||||||
|
if is_first_user do
|
||||||
|
socket.assigns.login_params
|
||||||
|
else
|
||||||
|
put_in(socket.assigns.login_params, [:messages, :info], "Some features may be unavailable until you confirm your email address. Check your inbox for instructions.")
|
||||||
|
end
|
||||||
|
|> put_in([:user_id], user.id)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
|
|> assign(login_params: login_params)
|
||||||
|> assign(trigger_submit: true)
|
|> assign(trigger_submit: true)
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %>
|
<i class="bi bi-journal-plus me-1"></i> Register
|
||||||
Register
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Create an account to manage your work shifts with us.</p>
|
<p class="lead">Create an account to manage your work shifts with us.</p>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %>
|
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="reg_form">
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
class: input_class(f, :email, "form-control"),
|
class: input_class(f, :email, "form-control"),
|
||||||
|
@ -25,8 +24,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -46,7 +45,7 @@
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
</.form>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
|
@ -4,6 +4,9 @@ defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, session, socket) do
|
def mount(_params, session, socket) do
|
||||||
user = Accounts.get_user!(session["user_id"])
|
user = Accounts.get_user!(session["user_id"])
|
||||||
|
@ -37,4 +40,6 @@ defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||||
|> assign(changeset: changeset)}
|
|> assign(changeset: changeset)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_registration, do: @app_allow_registration
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @socket, "bi-shield-lock", [class: "icon baseline"] %>
|
<i class="bi bi-shield-lock me-1"></i> Reset password
|
||||||
Reset password
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Hi <%= @user.email %> — tell us your new password, please.</p>
|
<p class="lead">Hi <%= @user.email %> — tell us your new password, please.</p>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "pw_reset_form"], fn f -> %>
|
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="pw_reset_form">
|
||||||
|
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -23,8 +22,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password_confirmation)}>
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
class: input_class(f, :password_confirmation, "form-control"),
|
class: input_class(f, :password_confirmation, "form-control"),
|
||||||
|
@ -43,10 +42,12 @@
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
</.form>
|
||||||
|
|
||||||
<p class="mt-3 is-pulled-right">
|
<p class="mt-3 is-pulled-right">
|
||||||
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
<%= if allow_registration() do %>
|
||||||
|
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
16
lib/shift73k_web/live/user/settings.html.heex
Normal file
16
lib/shift73k_web/live/user/settings.html.heex
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-11">
|
||||||
|
|
||||||
|
<h2 class="mb-3">
|
||||||
|
<i class="bi bi-sliders me-1"></i> User Settings
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="row justify-content-center justify-content-md-start">
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.Email} id={"email-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.Password} id={"password-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.WeekStart} id={"week_start-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
<.live_component module={Shift73kWeb.UserLive.Settings.CalendarUrl} id={"calendar_url-#{@current_user.id}"} current_user={@current_user} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,17 +0,0 @@
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-11">
|
|
||||||
|
|
||||||
<h2 class="mb-3">
|
|
||||||
<%= icon_div @socket, "bi-sliders", [class: "icon baseline"] %>
|
|
||||||
User Settings
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="row justify-content-center justify-content-md-start">
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Email, id: "email-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Password, id: "password-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.WeekStart, id: "week_start-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.CalendarUrl, id: "calendar_url-#{@current_user.id}", current_user: @current_user %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-6 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-6 mt-1">
|
||||||
|
|
||||||
<h3>iCal Subscribe URL</h3>
|
<h3>iCal Subscribe URL</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col mb-3">
|
<div class="col mb-3">
|
||||||
<label class="form-label">Use this URL to subscribe in calendar software</label>
|
<label class="form-label">Use this URL to subscribe in calendar software</label>
|
||||||
<input type="text" class="form-control" value="<%= Routes.user_shifts_ics_url(@socket, :index, @current_user.calendar_slug) %>" readonly onclick="this.focus();this.select()" />
|
<input type="text" class="form-control" value={Routes.user_shifts_ics_url(@socket, :index, @current_user.calendar_slug)} readonly onclick="this.focus();this.select()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Change email</h3>
|
<h3>Change email</h3>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
class: input_class(f, :email, "form-control"),
|
class: input_class(f, :email, "form-control"),
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
id: "user_email_current_password",
|
id: "user_email_current_password",
|
|
@ -1,12 +1,12 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Change password</h3>
|
<h3>Change password</h3>
|
||||||
|
|
||||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||||
|
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
|
@ -18,8 +18,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password_confirmation)}>
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-key-fill icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
class: input_class(f, :password_confirmation, "form-control"),
|
class: input_class(f, :password_confirmation, "form-control"),
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
id: "user_password_current_password",
|
id: "user_password_current_password",
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="<%= @id %>" class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
<div id={@id} class="col-12 col-sm-10 col-md-6 col-lg-5 col-xl-4 col-xxl-3 mt-1">
|
||||||
|
|
||||||
<h3>Calendar view</h3>
|
<h3>Calendar view</h3>
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<%= label cvf, :week_start_at, "Week starts at", class: "form-label" %>
|
<%= label cvf, :week_start_at, "Week starts at", class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @socket, "bi-calendar2-range", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-calendar2-range icon is-left text-muted fs-5"></i>
|
||||||
<%= select cvf, :week_start_at, week_start_options(),
|
<%= select cvf, :week_start_at, week_start_options(),
|
||||||
value: @current_user.week_start_at,
|
value: @current_user.week_start_at,
|
||||||
class: "form-select"
|
class: "form-select"
|
|
@ -9,9 +9,13 @@ defmodule Shift73kWeb.UserManagement.DeleteComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
|
def handle_event("confirm", %{"id" => id, "email" => email} = params, socket) do
|
||||||
id
|
IO.inspect(params)
|
||||||
|> Accounts.get_user()
|
|
||||||
|
user = Accounts.get_user(id)
|
||||||
|
IO.inspect(user)
|
||||||
|
|
||||||
|
user
|
||||||
|> Accounts.delete_user()
|
|> Accounts.delete_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
Are you sure you want to delete "<%= @delete_user.email %>"?
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= link "Confirm Delete", to: "#",
|
||||||
|
class: "btn btn-danger",
|
||||||
|
phx_click: "confirm",
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_value_id: @delete_user.id,
|
||||||
|
phx_value_email: @delete_user.email %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
Are you sure you want to delete "<%= @delete_user.email %>"?
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= link "Confirm Delete", to: "#",
|
|
||||||
class: "btn btn-danger",
|
|
||||||
phx_click: "confirm",
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_value_id: @delete_user.id,
|
|
||||||
phx_value_email: @delete_user.email %>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -33,7 +33,7 @@ defmodule Shift73kWeb.UserManagement.FormComponent do
|
||||||
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
||||||
case Accounts.register_user(user_params) do
|
case Accounts.register_user(user_params) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
{:ok, %Bamboo.Email{}} =
|
{:ok, _, %Swoosh.Email{} = _captured_email} =
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<%= form_for @changeset, "#", [
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_change: "validate",
|
||||||
|
phx_submit: "save"
|
||||||
|
], fn f -> %>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<%= label f, :email, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
|
||||||
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
|
<%= email_input f, :email,
|
||||||
|
value: input_value(f, :email),
|
||||||
|
class: input_class(f, :email, "form-control"),
|
||||||
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
maxlength: User.max_email,
|
||||||
|
autofocus: true,
|
||||||
|
phx_debounce: "250",
|
||||||
|
aria_describedby: error_ids(f, :email)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :email %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= label f, :password, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :password)}>
|
||||||
|
<i class="bi bi-key icon is-left text-muted fs-5"></i>
|
||||||
|
<%= password_input f, :password,
|
||||||
|
value: input_value(f, :password),
|
||||||
|
class: input_class(f, :password, "form-control"),
|
||||||
|
maxlength: User.max_password,
|
||||||
|
aria_describedby: error_ids(f, :password)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :password %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
||||||
|
<%= label f, :role, class: "form-label" %>
|
||||||
|
<div class="inner-addon left-addon mb-3">
|
||||||
|
<i class="bi bi-shield icon is-left text-muted fs-5"></i>
|
||||||
|
<%= select f, :role, Enum.map(User.roles(), fn {k, _v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
||||||
|
<span class="valid-feedback text-primary" style="display: block;">
|
||||||
|
<%= role_description(input_value(f, :role)) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
|
||||||
|
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
||||||
|
<%= submit "Save",
|
||||||
|
class: "btn btn-primary ",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,62 +0,0 @@
|
||||||
<%= form_for @changeset, "#", [
|
|
||||||
phx_target: @myself,
|
|
||||||
phx_change: "validate",
|
|
||||||
phx_submit: "save"
|
|
||||||
], fn f -> %>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= email_input f, :email,
|
|
||||||
value: input_value(f, :email),
|
|
||||||
class: input_class(f, :email, "form-control"),
|
|
||||||
placeholder: "e.g., babka@73k.us",
|
|
||||||
maxlength: User.max_email,
|
|
||||||
autofocus: true,
|
|
||||||
phx_debounce: "250",
|
|
||||||
aria_describedby: error_ids(f, :email)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :email %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= password_input f, :password,
|
|
||||||
value: input_value(f, :password),
|
|
||||||
class: input_class(f, :password, "form-control"),
|
|
||||||
maxlength: User.max_password,
|
|
||||||
aria_describedby: error_ids(f, :password)
|
|
||||||
%>
|
|
||||||
<%= error_tag f, :password %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
|
||||||
<%= label f, :role, class: "form-label" %>
|
|
||||||
<div class="inner-addon left-addon mb-3">
|
|
||||||
<%= icon_div @socket, "bi-shield", [class: "icon is-left text-muted fs-5"] %>
|
|
||||||
<%= select f, :role, Enum.map(User.roles(), fn {k, _v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
|
||||||
<span class="valid-feedback text-primary" style="display: block;">
|
|
||||||
<%= role_description(input_value(f, :role)) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
|
|
||||||
<%= link "Cancel", to: "#", class: "btn btn-outline-dark", phx_click: "hide", phx_target: "##{@modal_id}" %>
|
|
||||||
<%= submit "Save",
|
|
||||||
class: "btn btn-primary ",
|
|
||||||
disabled: !@changeset.valid?,
|
|
||||||
aria_disabled: !@changeset.valid? && "true" || false,
|
|
||||||
phx_disable_with: "Saving..."
|
|
||||||
%>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% end %>
|
|
|
@ -1,24 +1,34 @@
|
||||||
<%= if @live_action in [:new, :edit] do %>
|
<%= if @live_action in [:new, :edit] do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.UserManagement.FormComponent}
|
||||||
|
opts={[
|
||||||
id: @user.id || :new,
|
id: @user.id || :new,
|
||||||
title: @page_title,
|
title: @page_title,
|
||||||
action: @live_action,
|
action: @live_action,
|
||||||
user: @user,
|
user: @user,
|
||||||
current_user: @current_user %>
|
current_user: @current_user
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @delete_user do %>
|
<%= if @delete_user do %>
|
||||||
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
|
<.live_component
|
||||||
|
module={Shift73kWeb.ModalComponent}
|
||||||
|
id="modal"
|
||||||
|
component={Shift73kWeb.UserManagement.DeleteComponent}
|
||||||
|
opts={[
|
||||||
id: @delete_user.id,
|
id: @delete_user.id,
|
||||||
title: "Delete User",
|
title: "Delete User",
|
||||||
delete_user: @delete_user
|
delete_user: @delete_user
|
||||||
%>
|
]}
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<h2 class="mb-3">
|
<h2 class="mb-3">
|
||||||
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
|
<i class="bi bi-people me-1"></i> Listing Users
|
||||||
Listing Users
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<%# filtering and new item creation %>
|
<%# filtering and new item creation %>
|
||||||
|
@ -27,8 +37,7 @@
|
||||||
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
|
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
||||||
class: "btn btn-primary mb-3 mb-md-0" do %>
|
class: "btn btn-primary mb-3 mb-md-0" do %>
|
||||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
|
<i class="bi bi-person-plus me-1"></i> New User
|
||||||
New User
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,10 +45,10 @@
|
||||||
<%= form_for :sort, "#", [phx_change: "sort-by-change"], fn srt -> %>
|
<%= form_for :sort, "#", [phx_change: "sort-by-change"], fn srt -> %>
|
||||||
<%= label srt, :sort_by, class: "visually-hidden" %>
|
<%= label srt, :sort_by, class: "visually-hidden" %>
|
||||||
<div class="input-group inner-addon left-addon mb-3 mb-md-0">
|
<div class="input-group inner-addon left-addon mb-3 mb-md-0">
|
||||||
<%= icon_div @socket, "bi-arrow-down-up", [class: "icon is-left text-muted fs-5", style: "z-index:1001;"], [style: "padding: 1px;"] %>
|
<i class="bi bi-arrow-down-up icon is-left text-muted fs-5" style="z-index:1001;"></i>
|
||||||
<%= Phoenix.HTML.Form.select srt, :sort_by, ["Email": "email", "Role": "role", "Created at": "inserted_at"], value: @query.sort_by, class: "form-select" %>
|
<%= Phoenix.HTML.Form.select srt, :sort_by, ["Email": "email", "Role": "role", "Created at": "inserted_at"], value: @query.sort_by, class: "form-select rounded-start" %>
|
||||||
<button class="btn btn-primary" type="button" aria-label="Change sort order" phx-click="sort-order-change">
|
<button class="btn btn-primary" type="button" aria-label="Change sort order" phx-click="sort-order-change">
|
||||||
<%= icon_div @socket, (@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"), [class: "icon baseline"] %>
|
<i class={if @query.sort_order == "desc", do: "bi bi-sort-up-alt", else: "bi bi-sort-down-alt"}></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -49,9 +58,9 @@
|
||||||
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
||||||
<%= label flt, :filter, class: "visually-hidden" %>
|
<%= label flt, :filter, class: "visually-hidden" %>
|
||||||
<div class="inner-addon left-addon right-addon">
|
<div class="inner-addon left-addon right-addon">
|
||||||
<%= icon_div @socket, "bi-funnel", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-funnel icon is-left text-muted fs-5"></i>
|
||||||
<%= if @query.filter != "" do %>
|
<%= if @query.filter != "" do %>
|
||||||
<%= icon_div @socket, "bi-x-circle-fill", [class: "icon is-right text-primary fs-5"], [role: "img", aria_hidden: false, aria_label: "Clear filter", class: "cursor-pointer pe-auto", phx_click: "filter-clear"] %>
|
<i class="bi bi-x-circle-fill icon is-right text-primary fs-5 cursor-pointer pe-auto" role="img" aria-hidden="false" aria-label="Clear filter" phx-click="filter-clear"></i>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= text_input flt, :filter,
|
<%= text_input flt, :filter,
|
||||||
name: "filter",
|
name: "filter",
|
||||||
|
@ -92,21 +101,19 @@
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="d-inline d-sm-block col-auto">
|
<dd class="d-inline d-sm-block col-auto">
|
||||||
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
|
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
|
||||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :edit) do %>
|
<%= if Roles.can?(@current_user, user, :edit) do %>
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :delete) do %>
|
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={user.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -126,28 +133,25 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||||
Email
|
Email
|
||||||
<%= if @query.sort_by == "email", do: icon_div @socket,
|
<%= if @query.sort_by == "email" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||||
Role
|
Role
|
||||||
<%= if @query.sort_by == "role", do: icon_div @socket,
|
<%= if @query.sort_by == "role" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
<th scope="col" style="white-space: nowrap;" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||||
Created at
|
Created at
|
||||||
<%= if @query.sort_by == "inserted_at", do: icon_div @socket,
|
<%= if @query.sort_by == "inserted_at" do %>
|
||||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
<i class={@query.sort_order == "desc" && "bi bi-sort-up-alt ms-1" || "bi bi-sort-down-alt ms-1"}></i>
|
||||||
[class: "icon baseline ms-1"]
|
<% end %>
|
||||||
%>
|
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col">Confirmed?</th>
|
<th scope="col">Confirmed?</th>
|
||||||
|
@ -168,27 +172,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= for user <- @page.entries do %>
|
<%= for user <- @page.entries do %>
|
||||||
<tr id="user-<%= user.id %>">
|
<tr id={"user-#{user.id}"}>
|
||||||
<td class="align-middle"><%= user.email %></td>
|
<td class="align-middle"><%= user.email %></td>
|
||||||
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
||||||
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
|
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
|
||||||
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
|
<input type="checkbox" class="form-check-input" aria-hidden="true" checked={user.confirmed_at && :true || :false} disabled>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle text-end text-nowrap">
|
<td class="align-middle text-end text-nowrap">
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :edit) do %>
|
<%= if Roles.can?(@current_user, user, :edit) do %>
|
||||||
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
||||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
|
<i class="bi bi-pencil me-1"></i> Edit
|
||||||
Edit
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Roles.can?(@current_user, user, :delete) do %>
|
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
|
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id={user.id}>
|
||||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
|
<i class="bi bi-trash me-1"></i> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -227,16 +229,16 @@
|
||||||
<ul class="pagination mb-0">
|
<ul class="pagination mb-0">
|
||||||
|
|
||||||
<%# previous page button %>
|
<%# previous page button %>
|
||||||
<% icon = icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
|
||||||
<%= if @page.page_number == 1 do %>
|
<%= if @page.page_number == 1 do %>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-left"></i></span>
|
||||||
<span class="visually-hidden">Previous</span>
|
<span class="visually-hidden">Previous</span>
|
||||||
|
</li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number="<%= @page.page_number - 1 %>" phx-click="page-change"><%= icon %></a>
|
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number={@page.page_number - 1} phx-click="page-change"><i class="bi bi-chevron-left"></i></a>
|
||||||
<% end %>
|
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%# page buttons %>
|
<%# page buttons %>
|
||||||
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
||||||
|
@ -248,26 +250,26 @@
|
||||||
</li>
|
</li>
|
||||||
<% page_num == @page.page_number -> %>
|
<% page_num == @page.page_number -> %>
|
||||||
<li class="page-item active" aria-current="page">
|
<li class="page-item active" aria-current="page">
|
||||||
<span class="page-link"><%= page_num %></a>
|
<span class="page-link"><%= page_num %></span>
|
||||||
</li>
|
</li>
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" phx-value-page_number="<%= page_num %>" phx-click="page-change"><%= page_num %></a>
|
<a class="page-link" href="#" phx-value-page_number={page_num} phx-click="page-change"><%= page_num %></a>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%# next page button %>
|
<%# next page button %>
|
||||||
<% icon = icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %>
|
|
||||||
<%= if @page.page_number == @page.total_pages do %>
|
<%= if @page.page_number == @page.total_pages do %>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
<span class="page-link" aria-hidden="true"><i class="bi bi-chevron-right"></i></span>
|
||||||
<span class="visually-hidden">Next</span>
|
<span class="visually-hidden">Next</span>
|
||||||
|
</li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="#" aria-label="Next" phx-value-page_number="<%= @page.page_number + 1 %>" phx-click="page-change"><%= icon %></a>
|
<a class="page-link" href="#" aria-label="Next" phx-value-page_number={@page.page_number + 1} phx-click="page-change"><i class="bi bi-chevron-right"></i></a>
|
||||||
<% end %>
|
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
35
lib/shift73k_web/plugs/ensure_allow_registration_plug.ex
Normal file
35
lib/shift73k_web/plugs/ensure_allow_registration_plug.ex
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
defmodule Shift73kWeb.EnsureAllowRegistrationPlug do
|
||||||
|
@moduledoc """
|
||||||
|
This plug ensures that there is at least one known User.
|
||||||
|
"""
|
||||||
|
import Plug.Conn
|
||||||
|
import Phoenix.Controller
|
||||||
|
|
||||||
|
alias Shift73k.Repo
|
||||||
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec init(any()) :: any()
|
||||||
|
def init(config), do: config
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
|
||||||
|
def call(conn, _opts) do
|
||||||
|
# If there aren't even any users, or registration is allowed
|
||||||
|
if !Repo.exists?(User) || @app_allow_registration do
|
||||||
|
# We will allow registration
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
# Otherwise,
|
||||||
|
# if app is configured to not allow registration,
|
||||||
|
# and there is a user,
|
||||||
|
# then we redirect to root URL
|
||||||
|
conn
|
||||||
|
|> redirect(to: "/")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -27,8 +27,7 @@ defmodule Shift73kWeb.EnsureRolePlug do
|
||||||
def call(conn, roles) do
|
def call(conn, roles) do
|
||||||
user_token = get_session(conn, :user_token)
|
user_token = get_session(conn, :user_token)
|
||||||
|
|
||||||
(user_token &&
|
(user_token && Accounts.get_user_by_session_token(user_token))
|
||||||
Accounts.get_user_by_session_token(user_token))
|
|
||||||
|> has_role?(roles)
|
|> has_role?(roles)
|
||||||
|> maybe_halt(conn)
|
|> maybe_halt(conn)
|
||||||
end
|
end
|
||||||
|
|
30
lib/shift73k_web/plugs/ensure_user_exist_plug.ex
Normal file
30
lib/shift73k_web/plugs/ensure_user_exist_plug.ex
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
defmodule Shift73kWeb.EnsureUserExistPlug do
|
||||||
|
@moduledoc """
|
||||||
|
This plug ensures that there is at least one known User.
|
||||||
|
"""
|
||||||
|
import Plug.Conn
|
||||||
|
import Phoenix.Controller
|
||||||
|
|
||||||
|
alias Shift73k.Repo
|
||||||
|
alias Shift73k.Accounts.User
|
||||||
|
alias Shift73kWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec init(any()) :: any()
|
||||||
|
def init(config), do: config
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
|
||||||
|
def call(conn, _opts) do
|
||||||
|
# If there aren't even any users,
|
||||||
|
if !Repo.exists?(User) do
|
||||||
|
# We're just going to redirect to registration
|
||||||
|
conn
|
||||||
|
|> redirect(to: Routes.user_registration_path(conn, :new))
|
||||||
|
|> halt()
|
||||||
|
else
|
||||||
|
# Otherwise we proceed as normal
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,98 +2,100 @@ defmodule Shift73kWeb.Router do
|
||||||
use Shift73kWeb, :router
|
use Shift73kWeb, :router
|
||||||
import Shift73kWeb.UserAuth
|
import Shift73kWeb.UserAuth
|
||||||
alias Shift73kWeb.EnsureRolePlug
|
alias Shift73kWeb.EnsureRolePlug
|
||||||
|
alias Shift73kWeb.EnsureUserExistPlug
|
||||||
|
alias Shift73kWeb.EnsureAllowRegistrationPlug
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug(:accepts, ["html"])
|
plug :accepts, ["html"]
|
||||||
plug(:fetch_session)
|
plug :fetch_session
|
||||||
plug(:fetch_live_flash)
|
plug :fetch_live_flash
|
||||||
plug(:put_root_layout, {Shift73kWeb.LayoutView, :root})
|
plug :put_root_layout, {Shift73kWeb.LayoutView, :root}
|
||||||
plug(:protect_from_forgery)
|
plug :protect_from_forgery
|
||||||
plug(:put_secure_browser_headers)
|
plug :put_secure_browser_headers
|
||||||
plug(:fetch_current_user)
|
plug :fetch_current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :ensure_role_user do
|
||||||
plug(:accepts, ["json"])
|
plug EnsureRolePlug, [:admin, :manager, :user]
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :user do
|
pipeline :ensure_user_exist do
|
||||||
plug(EnsureRolePlug, [:admin, :manager, :user])
|
plug EnsureUserExistPlug
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :manager do
|
pipeline :ensure_allow_registration do
|
||||||
plug(EnsureRolePlug, [:admin, :manager])
|
plug EnsureAllowRegistrationPlug
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :admin do
|
pipeline :ensure_role_manager do
|
||||||
plug(EnsureRolePlug, :admin)
|
plug EnsureRolePlug, [:admin, :manager]
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
pipeline :ensure_role_admin do
|
||||||
pipe_through([:browser])
|
plug EnsureRolePlug, :admin
|
||||||
|
|
||||||
get("/", Redirector, to: "/assign")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Enables the Swoosh mailbox preview in development.
|
||||||
# scope "/api", Shift73kWeb do
|
|
||||||
# pipe_through :api
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Enables LiveDashboard only for development
|
|
||||||
#
|
#
|
||||||
# If you want to use the LiveDashboard in production, you should put
|
# Note that preview only shows emails that were sent by the same
|
||||||
# it behind authentication and allow only admins to access it.
|
# node running the Phoenix server.
|
||||||
# If your application does not have an admins-only section yet,
|
if Mix.env() == :dev do
|
||||||
# you can use Plug.BasicAuth to set up some basic authentication
|
scope "/dev" do
|
||||||
# as long as you are also using SSL (which you should anyway).
|
pipe_through :browser
|
||||||
if Mix.env() in [:dev, :test] do
|
|
||||||
import Phoenix.LiveDashboard.Router
|
|
||||||
|
|
||||||
scope "/" do
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||||
pipe_through(:browser)
|
|
||||||
live_dashboard("/dashboard", metrics: Shift73kWeb.Telemetry)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :redirect_if_user_is_authenticated])
|
pipe_through([:browser, :ensure_user_exist])
|
||||||
|
|
||||||
|
get "/", Redirector, to: "/assign"
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", Shift73kWeb do
|
||||||
|
pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_allow_registration]
|
||||||
|
|
||||||
|
get "/users/register", UserRegistrationController, :new
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", Shift73kWeb do
|
||||||
|
pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_user_exist]
|
||||||
|
|
||||||
# session routes, irrelevant if user is authenticated
|
# session routes, irrelevant if user is authenticated
|
||||||
get("/users/register", UserRegistrationController, :new)
|
get "/users/log_in", UserSessionController, :new
|
||||||
get("/users/log_in", UserSessionController, :new)
|
post "/users/log_in", UserSessionController, :create
|
||||||
post("/users/log_in", UserSessionController, :create)
|
get "/users/reset_password", UserResetPasswordController, :new
|
||||||
get("/users/reset_password", UserResetPasswordController, :new)
|
post "/users/reset_password", UserResetPasswordController, :create
|
||||||
post("/users/reset_password", UserResetPasswordController, :create)
|
get "/users/reset_password/:token", UserResetPasswordController, :edit
|
||||||
get("/users/reset_password/:token", UserResetPasswordController, :edit)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user])
|
pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
# user settings (change email, password, calendar week start, etc)
|
# user settings (change email, password, calendar week start, etc)
|
||||||
live("/users/settings", UserLive.Settings, :edit)
|
live "/users/settings", UserLive.Settings, :edit
|
||||||
|
|
||||||
# confirm email by token
|
# confirm email by token
|
||||||
get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
|
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser])
|
pipe_through [:browser, :ensure_user_exist]
|
||||||
|
|
||||||
# session paths
|
# session paths
|
||||||
delete("/users/log_out", UserSessionController, :delete)
|
delete "/users/log_out", UserSessionController, :delete
|
||||||
get("/users/force_logout", UserSessionController, :force_logout)
|
get "/users/force_logout", UserSessionController, :force_logout
|
||||||
get("/users/confirm", UserConfirmationController, :new)
|
get "/users/confirm", UserConfirmationController, :new
|
||||||
post("/users/confirm", UserConfirmationController, :create)
|
post "/users/confirm", UserConfirmationController, :create
|
||||||
get("/users/confirm/:token", UserConfirmationController, :confirm)
|
get "/users/confirm/:token", UserConfirmationController, :confirm
|
||||||
|
|
||||||
# ics/ical route for user's shifts
|
# ics/ical route for user's shifts
|
||||||
get("/ics/:slug", UserShiftsIcsController, :index)
|
get "/ics/:slug", UserShiftsIcsController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Shift73kWeb do
|
scope "/", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user, :user])
|
pipe_through [:browser, :require_authenticated_user, :ensure_role_user]
|
||||||
|
|
||||||
live "/templates", ShiftTemplateLive.Index, :index
|
live "/templates", ShiftTemplateLive.Index, :index
|
||||||
live "/templates/new", ShiftTemplateLive.Index, :new
|
live "/templates/new", ShiftTemplateLive.Index, :new
|
||||||
|
@ -111,16 +113,16 @@ defmodule Shift73kWeb.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
# scope "/", Shift73kWeb do
|
# scope "/", Shift73kWeb do
|
||||||
# pipe_through([:browser, :require_authenticated_user, :admin])
|
# pipe_through([:browser, :require_authenticated_user, :ensure_role_admin])
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# Users Management
|
# Users Management
|
||||||
scope "/users", Shift73kWeb do
|
scope "/users", Shift73kWeb do
|
||||||
pipe_through([:browser, :require_authenticated_user, :manager, :require_email_confirmed])
|
pipe_through [:browser, :require_authenticated_user, :ensure_role_manager, :require_email_confirmed]
|
||||||
|
|
||||||
live("/", UserManagementLive.Index, :index)
|
live "/", UserManagementLive.Index, :index
|
||||||
live("/new", UserManagementLive.Index, :new)
|
live "/new", UserManagementLive.Index, :new
|
||||||
live("/edit/:id", UserManagementLive.Index, :edit)
|
live "/edit/:id", UserManagementLive.Index, :edit
|
||||||
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-4">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1 class="fs-4 my-0 py-0 lh-base">
|
|
||||||
<%= link to: "/", class: "navbar-brand fs-4" do %>
|
|
||||||
<%= icon_div @conn, "bi-calendar2-week", [class: "icon baseline me-1"] %>
|
|
||||||
<span class="fw-light">Shift73k</span>
|
|
||||||
<% end %>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<%= if @current_user do %>
|
|
||||||
<button class="hamburger hamburger--squeeze collapsed navbar-toggler" id="navbarSupportedContentToggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="hamburger-box d-flex">
|
|
||||||
<span class="hamburger-inner"></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<% else %>
|
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-block d-lg-none") do %>
|
|
||||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
|
||||||
Log in
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
|
|
||||||
<%# nav LEFT items %>
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
|
|
||||||
<%#= if @current_user do %>
|
|
||||||
<%# <li class="nav-item"> %>
|
|
||||||
<%#= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "nav-link") do %>
|
|
||||||
<%#= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
|
|
||||||
<%# Templates %>
|
|
||||||
<%# end %>
|
|
||||||
<%# </li> %>
|
|
||||||
<%# end %>
|
|
||||||
|
|
||||||
<%# normal navbar link example %>
|
|
||||||
<%# <li class="nav-item"> %>
|
|
||||||
<%#= link "Properties", nav_link_opts(@conn, to: Routes.property_index_path(@conn, :index), class: "nav-link") %>
|
|
||||||
<%# </li> %>
|
|
||||||
|
|
||||||
<%# ACTIVE page link example %>
|
|
||||||
<%# <li class="nav-item">
|
|
||||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
<%# DISABLED page link example %>
|
|
||||||
<%# <li class="nav-item">
|
|
||||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
<%# normal dropdown menu example %>
|
|
||||||
<%# <li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownExample" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdownExample">
|
|
||||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
|
||||||
</ul>
|
|
||||||
</li> %>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<%# nav RIGHT items %>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
|
|
||||||
<%= if @current_user do %>
|
|
||||||
|
|
||||||
<%= render "navbar/_shifts_menu.html", assigns %>
|
|
||||||
|
|
||||||
<%= render "navbar/_user_menu.html", assigns %>
|
|
||||||
|
|
||||||
<% else %>
|
|
||||||
|
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-none d-lg-block") do %>
|
|
||||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
|
||||||
Log in
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
70
lib/shift73k_web/templates/layout/_navbar.html.heex
Normal file
70
lib/shift73k_web/templates/layout/_navbar.html.heex
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-4">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1 class="fs-4 my-0 py-0 lh-base">
|
||||||
|
<%= link to: "/", class: "navbar-brand fs-4" do %>
|
||||||
|
<i class="bi bi-calendar2-week me-1"></i>
|
||||||
|
<span class="fw-light">Shift73k</span>
|
||||||
|
<% end %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<%# If there's a current user,
|
||||||
|
OR if there are users & we allow registration,
|
||||||
|
THEN we will show a full menu configuration %>
|
||||||
|
|
||||||
|
<%= if @current_user || (Repo.exists?(User) && allow_registration()) do %>
|
||||||
|
|
||||||
|
<button class="hamburger hamburger--squeeze collapsed navbar-toggler" id="navbarSupportedContentToggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="hamburger-box d-flex">
|
||||||
|
<span class="hamburger-inner"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
|
||||||
|
<%# nav LEFT items %>
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<%# nav RIGHT items %>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
|
||||||
|
<%= if @current_user do %>
|
||||||
|
|
||||||
|
<%= render "navbar/_shifts_menu.html", assigns %>
|
||||||
|
|
||||||
|
<%= render "navbar/_user_menu.html", assigns %>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
|
||||||
|
<%= render "navbar/_nouser_menu.html", assigns %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# If there's no current user,
|
||||||
|
AND:
|
||||||
|
There are no users -- [REGISTER]
|
||||||
|
OR no registration allowed -- [LOG IN] %>
|
||||||
|
<% else %>
|
||||||
|
|
||||||
|
<%= if !Repo.exists?(User) || allow_registration() do %>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "btn btn-outline-light") do %>
|
||||||
|
<i class="bi bi-journal-plus"></i> Register
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %>
|
||||||
|
<i class="bi bi-door-open"></i> Log in
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
10
lib/shift73k_web/templates/layout/_preamble.html.heex
Normal file
10
lib/shift73k_web/templates/layout/_preamble.html.heex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<%= if dev_env?() do %>
|
||||||
|
<script type="module" src="http://localhost:3000/@vite/client"></script>
|
||||||
|
<script type="module" src="http://localhost:3000/js/app.js"></script>
|
||||||
|
<% else %>
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-300-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-400-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link rel="preload" href={Routes.static_path(@conn, "/assets/lato-latin-700-normal.woff2")} as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
|
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
|
||||||
|
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
|
||||||
|
<% end %>
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
||||||
<%= for {kind, class} <- alert_kinds() do %>
|
<%= for {kind, class} <- alert_kinds() do %>
|
||||||
<%= if flash_content = get_flash(@conn, kind) do %>
|
<%= if flash_content = get_flash(@conn, kind) do %>
|
||||||
<div class="alert <%= class %> alert-dismissible fade show" role="alert">
|
<div class={"alert #{class} alert-dismissible fade show"} role="alert">
|
||||||
<%= flash_content %>
|
<%= flash_content %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
<div class="col-md-12 col-lg-10 col-xxl-8 ">
|
||||||
<%= for {kind, class} <- alert_kinds() do %>
|
<%= for {kind, class} <- alert_kinds() do %>
|
||||||
<%= if flash_content = live_flash(@flash, kind) do %>
|
<%= if flash_content = live_flash(@flash, kind) do %>
|
||||||
<div class="alert <%= class %> alert-dismissible fade show" role="alert" id="lv-alert-<%= kind %>" phx-hook="AlertRemover" data-key="<%= kind %>">
|
<div class={"alert #{class} alert-dismissible fade show"} role="alert" id={"lv-alert-#{kind}"} phx-hook="AlertRemover" data-key={"#{kind}"}>
|
||||||
<%= flash_content %>
|
<%= flash_content %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownNoUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-person-circle me-1"></i> Hello?
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownNoUserMenu">
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
|
<i class="bi bi-journal-plus me-1"></i> Register
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
|
<i class="bi bi-door-open me-1"></i> Log in
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</li>
|
|
@ -1,28 +1,24 @@
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownShiftsMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline me-1"] %>
|
<i class="bi bi-calendar2 me-1"></i> Shifts
|
||||||
Shifts
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownShiftsMenu">
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_assign_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-calendar2-plus", [class: "icon baseline me-1"] %>
|
<i class="bi bi-calendar2-plus me-1"></i> Schedule Shifts
|
||||||
Schedule Shifts
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-card-list", [class: "icon baseline me-1"] %>
|
<i class="bi bi-card-list me-1"></i> My Scheduled Shifts
|
||||||
My Scheduled Shifts
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
|
<i class="bi bi-clock-history me-1"></i> My Shift Templates
|
||||||
My Shift Templates
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -30,14 +26,12 @@
|
||||||
<%# user_shifts_csv_path %>
|
<%# user_shifts_csv_path %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline me-1"] %>
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||||
CSV Export
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.shift_import_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.shift_import_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-box-arrow-in-left", [class: "icon baseline me-1"] %>
|
<i class="bi bi-box-arrow-in-left me-1"></i> iCal Import
|
||||||
iCal Import
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<%= icon_div @conn, "bi-person-circle", [class: "icon baseline me-1"] %>
|
<i class="bi bi-person-circle me-1"></i> Hello!
|
||||||
<%= @current_user && "Hello!" || "Hello?" %>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
|
||||||
|
@ -12,22 +11,19 @@
|
||||||
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-people", [class: "icon baseline me-1"] %>
|
<i class="bi bi-people me-1"></i> Users
|
||||||
Users
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-sliders", [class: "icon baseline me-1"] %>
|
<i class="bi bi-sliders me-1"></i> Settings
|
||||||
Settings
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-door-closed", [class: "icon baseline me-1"] %>
|
<i class="bi bi-door-closed me-1"></i> Log out
|
||||||
Log out
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
28
lib/shift73k_web/templates/layout/root.html.heex
Normal file
28
lib/shift73k_web/templates/layout/root.html.heex
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<%= csrf_meta_tag() %>
|
||||||
|
<%= live_title_tag assigns[:page_title] || "", suffix: assigns[:page_title] && " · Shift73k" || "Shift73k" %>
|
||||||
|
<%= render "_preamble.html", assigns %>
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href={Routes.static_path(@conn, "/apple-touch-icon.png")}>
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href={Routes.static_path(@conn, "/favicon-32x32.png")}>
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href={Routes.static_path(@conn, "/favicon-16x16.png")}>
|
||||||
|
<link rel="manifest" href={Routes.static_path(@conn, "/site.webmanifest")}>
|
||||||
|
<link rel="mask-icon" href={Routes.static_path(@conn, "/safari-pinned-tab.svg")} color="#78868a">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Shift73k">
|
||||||
|
<meta name="application-name" content="Shift73k">
|
||||||
|
<meta name="msapplication-TileColor" content="#ee6c4d">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<%= render "_navbar.html", assigns %>
|
||||||
|
|
||||||
|
<%= @inner_content %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,32 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<%= csrf_meta_tag() %>
|
|
||||||
<%= live_title_tag assigns[:page_title] || "", suffix: assigns[:page_title] && " · Shift73k" || "Shift73k" %>
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-300-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-400-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="preload" href="<%= Routes.static_path(@conn, "/fonts/lato-latin-700-normal.woff2") %>" as="font" type="font/woff2" crossorigin="anonymous">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= Routes.static_path(@conn, "/apple-touch-icon.png") %>">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= Routes.static_path(@conn, "/favicon-32x32.png") %>">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= Routes.static_path(@conn, "/favicon-16x16.png") %>">
|
|
||||||
<link rel="manifest" href="<%= Routes.static_path(@conn, "/site.webmanifest") %>">
|
|
||||||
<link rel="mask-icon" href="<%= Routes.static_path(@conn, "/safari-pinned-tab.svg") %>" color="#78868a">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Shift73k">
|
|
||||||
<meta name="application-name" content="Shift73k">
|
|
||||||
<meta name="msapplication-TileColor" content="#ee6c4d">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
<link rel="icon" href="favicon.ico">
|
|
||||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
|
||||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<%= render "_navbar.html", assigns %>
|
|
||||||
|
|
||||||
<%= @inner_content %>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-arrow-repeat", [class: "icon baseline"] %>
|
<i class="bi bi-arrow-repeat me-1"></i> Resend confirmation instructions
|
||||||
Resend confirmation instructions
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">We'll send you another email with instructions to confirm your email address.</p>
|
<p class="lead">We'll send you another email with instructions to confirm your email address.</p>
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: @current_user && @current_user.email || "",
|
value: @current_user && @current_user.email || "",
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
@ -30,6 +29,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
<%= if allow_registration() do %>
|
||||||
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
<div class="col-11 col-sm-10 col-md-8 col-lg-7 col-xl-6 col-xxl-5">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "mdi-head-question-outline", [class: "icon baseline"] %>
|
<i class="bi bi-patch-question me-1"></i> Forgot your password?
|
||||||
Forgot your password?
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">We'll send you an email with instructions to reset your password.</p>
|
<p class="lead">We'll send you an email with instructions to reset your password.</p>
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
|
@ -28,7 +27,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= if allow_registration() do %>
|
||||||
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
<div class="col-11 col-sm-8 col-md-6 col-lg-5 col-xl-4 col-xxl-3">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
|
<i class="bi bi-door-open me-1"></i> Log in
|
||||||
Log in
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Who goes there?</p>
|
<p class="lead">Who goes there?</p>
|
||||||
|
|
||||||
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "needs-validation", novalidate: true], fn f -> %>
|
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "needs-validation", novalidate: true], fn f -> %>
|
||||||
<%= if @error_message do %>
|
<%= if @error_message do %>
|
||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
<div class="alert alert-danger alert-dismissible fade show mt-4" role="alert">
|
||||||
<%= @error_message %>
|
<%= @error_message %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,11 +16,12 @@
|
||||||
|
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-at icon is-left text-muted fs-5"></i>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
maxlength: User.max_email,
|
maxlength: User.max_email,
|
||||||
|
autofocus: true,
|
||||||
required: true
|
required: true
|
||||||
%>
|
%>
|
||||||
<span class="invalid-feedback">must be a valid email address</span>
|
<span class="invalid-feedback">must be a valid email address</span>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="inner-addon left-addon mb-3">
|
<div class="inner-addon left-addon mb-3">
|
||||||
<%= icon_div @conn, "bi-lock", [class: "icon is-left text-muted fs-5"] %>
|
<i class="bi bi-lock icon is-left text-muted fs-5"></i>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
required: true
|
required: true
|
||||||
|
@ -49,7 +49,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
<%= if allow_registration() do %>
|
||||||
|
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||||
|
<% end %>
|
||||||
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline"] %>
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> CSV Export
|
||||||
CSV Export
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.</p>
|
<p class="lead">Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.</p>
|
||||||
|
|
||||||
|
@ -27,6 +26,7 @@
|
||||||
value: Date.beginning_of_month(today),
|
value: Date.beginning_of_month(today),
|
||||||
min: min_date,
|
min: min_date,
|
||||||
max: max_date,
|
max: max_date,
|
||||||
|
required: true,
|
||||||
class: "form-control"
|
class: "form-control"
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
value: Date.end_of_month(today),
|
value: Date.end_of_month(today),
|
||||||
min: min_date,
|
min: min_date,
|
||||||
max: max_date,
|
max: max_date,
|
||||||
|
required: true,
|
||||||
class: "form-control"
|
class: "form-control"
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
|
@ -2,8 +2,7 @@
|
||||||
<div class="col-12 col-md-10 col-xl-8">
|
<div class="col-12 col-md-10 col-xl-8">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
<%= icon_div @conn, "bi-calendar2", [class: "icon baseline"] %>
|
<i class="bi bi-calendar2 me-1"></i> User Shifts ICS
|
||||||
User Shifts ICS
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">Shifts for user: <%= @user.email %></p>
|
<p class="lead">Shifts for user: <%= @user.email %></p>
|
||||||
<p>Calendar slug: <%= @slug %></p>
|
<p>Calendar slug: <%= @slug %></p>
|
|
@ -1,38 +0,0 @@
|
||||||
defmodule Shift73kWeb.IconHelpers do
|
|
||||||
@moduledoc """
|
|
||||||
Generate SVG sprite use tags for SVG icons
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Phoenix.HTML
|
|
||||||
alias Shift73kWeb.Router.Helpers, as: Routes
|
|
||||||
|
|
||||||
def icon_div(conn, name, div_opts \\ [], svg_opts \\ []) do
|
|
||||||
content_tag(:div, tag_opts(name, div_opts)) do
|
|
||||||
icon_svg(conn, name, svg_opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def icon_svg(conn, name, opts \\ []) do
|
|
||||||
opts = aria_hidden?(opts)
|
|
||||||
|
|
||||||
content_tag(:svg, tag_opts(name, opts)) do
|
|
||||||
~E"""
|
|
||||||
<%= if title = Keyword.get(opts, :aria_label), do: content_tag(:title, title) %>
|
|
||||||
<%= tag(:use, "xlink:href": Routes.static_path(conn, "/images/icons.svg##{name}")) %>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp tag_opts(name, opts) do
|
|
||||||
Keyword.update(opts, :class, name, fn c -> "#{c} #{name}" end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp aria_hidden?(opts) do
|
|
||||||
case Keyword.get(opts, :aria_hidden) do
|
|
||||||
"false" -> Keyword.drop(opts, [:aria_hidden])
|
|
||||||
false -> Keyword.drop(opts, [:aria_hidden])
|
|
||||||
"true" -> opts
|
|
||||||
_ -> Keyword.put(opts, :aria_hidden, "true")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,9 +1,21 @@
|
||||||
defmodule Shift73kWeb.LayoutView do
|
defmodule Shift73kWeb.LayoutView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
|
alias Shift73k.Repo
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
alias Shift73kWeb.Roles
|
alias Shift73kWeb.Roles
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
|
# With a Vite.js-based workflow, we will import different asset files in development
|
||||||
|
# and in production builds. Therefore, we will need a way to conditionally render
|
||||||
|
# <script> tags based on Mix environment. However, since Mix is not available in
|
||||||
|
# releases, we need to cache the Mix environment at compile time. To this end:
|
||||||
|
@env Mix.env() # remember value at compile time
|
||||||
|
def dev_env?, do: @env == :dev
|
||||||
|
|
||||||
|
def allow_registration, do: @app_allow_registration
|
||||||
|
|
||||||
def nav_link_opts(conn, opts) do
|
def nav_link_opts(conn, opts) do
|
||||||
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
||||||
false -> opts
|
false -> opts
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule Shift73kWeb.UserConfirmationView do
|
defmodule Shift73kWeb.UserConfirmationView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
|
def allow_registration, do: @app_allow_registration
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule Shift73kWeb.UserResetPasswordView do
|
defmodule Shift73kWeb.UserResetPasswordView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
|
def allow_registration, do: @app_allow_registration
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule Shift73kWeb.UserSessionView do
|
defmodule Shift73kWeb.UserSessionView do
|
||||||
use Shift73kWeb, :view
|
use Shift73kWeb, :view
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
|
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
|
||||||
|
@app_allow_registration @app_vars[:allow_registration]
|
||||||
|
|
||||||
|
def allow_registration, do: @app_allow_registration
|
||||||
end
|
end
|
||||||
|
|
37
mix.exs
37
mix.exs
|
@ -4,10 +4,10 @@ defmodule Shift73k.MixProject do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :shift73k,
|
app: :shift73k,
|
||||||
version: "0.1.0",
|
version: "0.1.1",
|
||||||
elixir: "~> 1.7",
|
elixir: "~> 1.12",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
compilers: Mix.compilers(),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps()
|
deps: deps()
|
||||||
|
@ -33,23 +33,21 @@ defmodule Shift73k.MixProject do
|
||||||
# Type `mix help deps` for examples and options.
|
# Type `mix help deps` for examples and options.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:bcrypt_elixir, "~> 2.0"},
|
{:bcrypt_elixir, "~> 3.0"},
|
||||||
{:phoenix, "~> 1.5.8"},
|
{:phoenix, "~> 1.6.11"},
|
||||||
{:phoenix_ecto, "~> 4.1"},
|
{:phoenix_ecto, "~> 4.4"},
|
||||||
{:ecto_sql, "~> 3.4"},
|
{:ecto_sql, "~> 3.6"},
|
||||||
{:postgrex, ">= 0.0.0"},
|
{:postgrex, ">= 0.0.0"},
|
||||||
{:phoenix_live_view, "~> 0.15.0"},
|
{:phoenix_html, "~> 3.0"},
|
||||||
{:floki, ">= 0.27.0", only: :test},
|
|
||||||
{:phoenix_html, "~> 2.11"},
|
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_dashboard, "~> 0.4"},
|
{:phoenix_live_view, "~> 0.17.5"},
|
||||||
{:telemetry_metrics, "~> 0.4"},
|
{:floki, ">= 0.30.0", only: :test},
|
||||||
{:telemetry_poller, "~> 0.4"},
|
{:swoosh, "~> 1.7"},
|
||||||
{:gettext, "~> 0.11"},
|
{:gen_smtp, "~> 1.2"},
|
||||||
{:jason, "~> 1.0"},
|
{:telemetry_metrics, "~> 0.6"},
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:telemetry_poller, "~> 1.0"},
|
||||||
{:bamboo, "~> 2.0"},
|
{:jason, "~> 1.2"},
|
||||||
{:bamboo_smtp, "~> 4.0"},
|
{:plug_cowboy, "~> 2.5"},
|
||||||
{:scrivener_ecto, "~> 2.0"},
|
{:scrivener_ecto, "~> 2.0"},
|
||||||
{:tzdata, "~> 1.1"},
|
{:tzdata, "~> 1.1"},
|
||||||
{:nimble_csv, "~> 1.0"},
|
{:nimble_csv, "~> 1.0"},
|
||||||
|
@ -58,8 +56,7 @@ defmodule Shift73k.MixProject do
|
||||||
|
|
||||||
# Additional packages
|
# Additional packages
|
||||||
|
|
||||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.5", only: [:dev, :test], runtime: false}
|
||||||
{:sobelow, "~> 0.8", only: :dev}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
82
mix.lock
82
mix.lock
|
@ -1,60 +1,54 @@
|
||||||
%{
|
%{
|
||||||
"bamboo": {:hex, :bamboo, "2.0.2", "0e2914d2bea0de3b1743384c24ffbe20fbb58094376a49f1cf5d9ed9959abd82", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "058d57cf4fcdac19413aa72732eb43c88954fb151a1cb6a382014e0cddbf6314"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
|
||||||
"bamboo_smtp": {:hex, :bamboo_smtp, "4.0.0", "0cc7df161d5d440d280a6d2eb20bf80bc45ea77161728a229e5ab339dcd087cd", [:mix], [{:bamboo, "~> 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.1.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "2412015092121b9f24f3f2e654bcd98e5c5f9afb323a94f8defa22e70ba8f23d"},
|
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"},
|
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||||
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
"comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
|
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||||
"credo": {:hex, :credo, "1.5.5", "e8f422026f553bc3bebb81c8e8bf1932f498ca03339856c7fec63d3faac8424b", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dd8623ab7091956a855dc9f3062486add9c52d310dfd62748779c4315d8247de"},
|
"credo": {:hex, :credo, "1.6.6", "f51f8d45db1af3b2e2f7bee3e6d3c871737bda4a91bff00c5eec276517d1a19c", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "625520ce0984ee0f9f1f198165cd46fa73c1e59a17ebc520038b8fce056a5bdc"},
|
||||||
"db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
|
"db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
|
||||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||||
"ecto": {:hex, :ecto, "3.5.8", "8ebf12be6016cb99313348ba7bb4612f4114b9a506d6da79a2adc7ef449340bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea0be182ea8922eb7742e3ae8e71b67ee00ae177de1bf76210299a5f16ba4c77"},
|
"ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"},
|
||||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
"ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
|
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||||
"floki": {:hex, :floki, "0.30.1", "75d35526d3a1459920b6e87fdbc2e0b8a3670f965dd0903708d2b267e0904c55", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e9c03524447d1c4cbfccd672d739b8c18453eee377846b119d4fd71b1a176bb8"},
|
"floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"},
|
||||||
"gen_smtp": {:hex, :gen_smtp, "1.1.0", "b0c92138f69e2f73e1eb791075e93e952efcbc231a536740749b02a1a57155a3", [:rebar3], [{:hut, "1.3.0", [hex: :hut, repo: "hexpm", optional: false]}, {:ranch, ">= 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e53a13c4bb662331bdf4aa47f00982ef49ce2b4e5c703240542cb7b28f23546a"},
|
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
|
||||||
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
|
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||||
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
|
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||||
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
"icalendar": {:hex, :icalendar, "1.1.2", "5d0afff5d0143c5bd43f18ae32a777bf0fb9a724543ab05229a460d368f0a5e7", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2060f8e353fdf3047e95a3f012583dc3c0bbd7ca1010e32ed9e9fc5760ad4292"},
|
||||||
"icalendar": {:hex, :icalendar, "1.1.0", "898a8640abb32d161d990e419999004718a7a4b48be31f48db248f90ca33fa6e", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "a131f45fbabd2ee5a22e6bc49ea91e81131158394e7169274cee866263640dca"},
|
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
|
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||||
"nimble_csv": {:hex, :nimble_csv, "1.1.0", "b1dba4a86be9e03065c9de829050468e591f569100332db949e7ce71be0afc25", [:mix], [], "hexpm", "e986755bc302832cac429be6deda0fc9d82d3c82b47abefb68b3c17c9d949a3f"},
|
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||||
"phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"},
|
"phoenix": {:hex, :phoenix, "1.6.11", "29f3c0fd12fa1fc4d4b05e341578e55bc78d96ea83a022587a7e276884d397e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1664e34f80c25ea4918fbadd957f491225ef601c0e00b4e644b1a772864bfbc2"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
||||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
|
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.11", "205f6aa5405648c76f2abcd57716f42fc07d8f21dd8ea7b262dd12b324b50c95", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7177791944b7f90ed18f5935a6a5f07f760b36f7b3bdfb9d28c57440a3c43f99"},
|
||||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
|
||||||
"phx_gen_auth": {:hex, :phx_gen_auth, "0.6.0", "4ffbfa5b34ad8178c3dfcb996fed776df425903595cbc8d56a9ae5bc53136810", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9a801c0f0bc251d8d91d62cecba0ebb6a90b8580fa8843029d931d15164e6ad9"},
|
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
|
||||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
|
|
||||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||||
"postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"},
|
"postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"},
|
||||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||||
"swoosh": {:hex, :swoosh, "1.3.2", "608819a638d527d0fd6e8892db431edd3f93c0452499880f94a80bbbcf8377e6", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "2c77c2eef273de37283bdc09c6506d466928f897600ea258fbb4765bbffd6ab2"},
|
"swoosh": {:hex, :swoosh, "1.7.4", "f967d9b2659e81bab241b96267aae1001d35c2beea2df9c03dcf47b007bf566f", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1553d994b4cf069162965e63de1e1c53d8236e127118d21e56ce2abeaa3f25b4"},
|
||||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||||
"timex": {:hex, :timex, "3.7.3", "df8a2ea814749d700d6878ab9eacac9fdb498ecee2f507cb0002ec172bc24d0f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8691c1d86ca3a7bc14a156e2199dc8927be95d1a8f0e3b69e4bb2d6262c53ac6"},
|
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
|
||||||
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
|
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,28 +5,28 @@ defmodule Shift73k.Repo.Migrations.CreateUsersAuthTables do
|
||||||
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
|
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
|
||||||
|
|
||||||
create table(:users, primary_key: false) do
|
create table(:users, primary_key: false) do
|
||||||
add(:id, :binary_id, primary_key: true)
|
add :id, :binary_id, primary_key: true
|
||||||
add(:email, :citext, null: false)
|
add :email, :citext, null: false
|
||||||
add(:hashed_password, :string, null: false)
|
add :hashed_password, :string, null: false
|
||||||
add(:role, :string, null: false)
|
add :role, :string, null: false
|
||||||
add(:confirmed_at, :naive_datetime)
|
add :confirmed_at, :naive_datetime
|
||||||
add(:week_start_at, :string, null: false)
|
add :week_start_at, :string, null: false
|
||||||
add(:calendar_slug, :string, null: false)
|
add :calendar_slug, :string, null: false
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
create(unique_index(:users, [:email, :calendar_slug]))
|
create unique_index(:users, [:email, :calendar_slug])
|
||||||
|
|
||||||
create table(:users_tokens, primary_key: false) do
|
create table(:users_tokens, primary_key: false) do
|
||||||
add(:id, :binary_id, primary_key: true)
|
add :id, :binary_id, primary_key: true
|
||||||
add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false)
|
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
|
||||||
add(:token, :binary, null: false)
|
add :token, :binary, null: false
|
||||||
add(:context, :string, null: false)
|
add :context, :string, null: false
|
||||||
add(:sent_to, :string)
|
add :sent_to, :string
|
||||||
timestamps(updated_at: false)
|
timestamps(updated_at: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
create(index(:users_tokens, [:user_id]))
|
create index(:users_tokens, [:user_id])
|
||||||
create(unique_index(:users_tokens, [:context, :token]))
|
create unique_index(:users_tokens, [:context, :token])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Shift73k.Repo.Migrations.AddUserDefaultShiftColumn do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add(:fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :nilify_all))
|
add :fave_shift_template_id, references(:shift_templates, type: :binary_id, on_delete: :nilify_all)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Shift73k.Repo.Migrations.FixShiftsUserIdReference do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("ALTER TABLE shifts DROP CONSTRAINT shifts_user_id_fkey")
|
||||||
|
|
||||||
|
alter table(:shifts) do
|
||||||
|
modify :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("ALTER TABLE shifts DROP CONSTRAINT shifts_user_id_fkey")
|
||||||
|
|
||||||
|
alter table(:shifts) do
|
||||||
|
modify :user_id, references(:users, on_delete: :nothing, type: :binary_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,139 +14,150 @@ alias Shift73k.Repo
|
||||||
alias Shift73k.Accounts
|
alias Shift73k.Accounts
|
||||||
alias Shift73k.Accounts.User
|
alias Shift73k.Accounts.User
|
||||||
|
|
||||||
############################################################################
|
|
||||||
## INSERTING MOCK USER DATA
|
|
||||||
|
|
||||||
{:ok, _admin} =
|
if Mix.env() == :dev do
|
||||||
Accounts.register_user(%{
|
|
||||||
email: "admin@company.com",
|
|
||||||
password: "123456789abC",
|
|
||||||
password_confirmation: "123456789abC",
|
|
||||||
role: Accounts.registration_role()
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, _user_1} =
|
if System.get_env("ECTO_SEED_DB") do
|
||||||
Accounts.register_user(%{
|
|
||||||
email: "user1@company.com",
|
|
||||||
password: "123456789abC",
|
|
||||||
password_confirmation: "123456789abC",
|
|
||||||
role: Accounts.registration_role()
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, _user_2} =
|
############################################################################
|
||||||
Accounts.register_user(%{
|
## INSERTING MOCK USER DATA
|
||||||
email: "user2@company.com",
|
|
||||||
password: "123456789abC",
|
|
||||||
password_confirmation: "123456789abC",
|
|
||||||
role: Accounts.registration_role()
|
|
||||||
})
|
|
||||||
|
|
||||||
# if Mix.env() == :dev do
|
{:ok, _admin} =
|
||||||
this_path = Path.dirname(__ENV__.file)
|
Accounts.register_user(%{
|
||||||
users_json = Path.join(this_path, "MOCK_DATA_users.json")
|
email: "admin@company.com",
|
||||||
|
password: "123456789abC",
|
||||||
|
password_confirmation: "123456789abC",
|
||||||
|
role: Accounts.registration_role()
|
||||||
|
})
|
||||||
|
|
||||||
count_to_take = 15
|
{:ok, _user_1} =
|
||||||
|
Accounts.register_user(%{
|
||||||
|
email: "user1@company.com",
|
||||||
|
password: "123456789abC",
|
||||||
|
password_confirmation: "123456789abC",
|
||||||
|
role: Accounts.registration_role()
|
||||||
|
})
|
||||||
|
|
||||||
mock_users = users_json |> File.read!() |> Jason.decode!() |> Enum.take_random(count_to_take)
|
{:ok, _user_2} =
|
||||||
|
Accounts.register_user(%{
|
||||||
|
email: "user2@company.com",
|
||||||
|
password: "123456789abC",
|
||||||
|
password_confirmation: "123456789abC",
|
||||||
|
role: Accounts.registration_role()
|
||||||
|
})
|
||||||
|
|
||||||
extra_mock_users = ~s([
|
# if Mix.env() == :dev do
|
||||||
{"email":"adam@73k.us","password":"adamadamA1","role":"admin","inserted_at":"2018-12-14T01:01:01Z","confirmed_at":true},
|
this_path = Path.dirname(__ENV__.file)
|
||||||
{"email":"karen@73k.us","password":"karenkarenA1","role":"manager","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":true},
|
users_json = Path.join(this_path, "MOCK_DATA_users.json")
|
||||||
{"email":"kat@73k.us","password":"katkatA1","role":"manager","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":true}
|
|
||||||
])
|
|
||||||
|
|
||||||
# for random week_start_at values
|
count_to_take = 15
|
||||||
[head | tail] = Shift73k.weekdays()
|
|
||||||
week_starts = [head | Enum.drop(tail, 4)]
|
|
||||||
|
|
||||||
mock_users =
|
mock_users = users_json |> File.read!() |> Jason.decode!() |> Enum.take_random(count_to_take)
|
||||||
extra_mock_users
|
|
||||||
|> Jason.decode!()
|
|
||||||
|> Stream.concat(mock_users)
|
|
||||||
|> Enum.map(fn e ->
|
|
||||||
add_dt = NaiveDateTime.from_iso8601!(e["inserted_at"])
|
|
||||||
|
|
||||||
%{
|
extra_mock_users = ~s([
|
||||||
email: e["email"],
|
{"email":"adam@73k.us","password":"adamadamA1","role":"admin","inserted_at":"2018-12-14T01:01:01Z","confirmed_at":true},
|
||||||
role: String.to_existing_atom(e["role"]),
|
{"email":"kat@73k.us","password":"katkatA1","role":"manager","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":true},
|
||||||
hashed_password: Bcrypt.hash_pwd_salt(e["password"]),
|
{"email":"babka@73k.us","password":"Babka2020","role":"user","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":false},
|
||||||
week_start_at: Enum.at(week_starts, Enum.random(0..2)),
|
{"email":"malcolm@73k.us","password":"Malc2018","role":"user","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":false},
|
||||||
calendar_slug: Ecto.UUID.generate(),
|
{"email":"casio@73k.us","password":"Casio2011","role":"user","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":false}
|
||||||
inserted_at: add_dt,
|
])
|
||||||
updated_at: add_dt,
|
|
||||||
confirmed_at: (e["confirmed_at"] && NaiveDateTime.add(add_dt, 300, :second)) || nil
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Repo.insert_all(User, mock_users)
|
# for random week_start_at values
|
||||||
# end
|
[head | tail] = Shift73k.weekdays()
|
||||||
|
week_starts = [head | Enum.drop(tail, 4)]
|
||||||
|
|
||||||
#####
|
mock_users =
|
||||||
# shift tepmlates
|
extra_mock_users
|
||||||
alias Shift73k.Shifts.Templates.ShiftTemplate
|
|> Jason.decode!()
|
||||||
|
|> Stream.concat(mock_users)
|
||||||
|
|> Enum.map(fn e ->
|
||||||
|
add_dt = NaiveDateTime.from_iso8601!(e["inserted_at"])
|
||||||
|
|
||||||
shifts_json = Path.join(this_path, "MOCK_DATA_shift-templates.json")
|
%{
|
||||||
mock_shifts = shifts_json |> File.read!() |> Jason.decode!()
|
email: e["email"],
|
||||||
|
role: String.to_existing_atom(e["role"]),
|
||||||
|
hashed_password: Bcrypt.hash_pwd_salt(e["password"]),
|
||||||
|
week_start_at: Enum.at(week_starts, Enum.random(0..2)),
|
||||||
|
calendar_slug: Ecto.UUID.generate(),
|
||||||
|
inserted_at: add_dt,
|
||||||
|
updated_at: add_dt,
|
||||||
|
confirmed_at: (e["confirmed_at"] && NaiveDateTime.add(add_dt, 300, :second)) || nil
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Repo.insert_all(User, mock_users)
|
||||||
|
# end
|
||||||
|
|
||||||
|
#####
|
||||||
|
# shift tepmlates
|
||||||
|
alias Shift73k.Shifts.Templates.ShiftTemplate
|
||||||
|
|
||||||
|
shifts_json = Path.join(this_path, "MOCK_DATA_shift-templates.json")
|
||||||
|
mock_shifts = shifts_json |> File.read!() |> Jason.decode!()
|
||||||
|
|
||||||
|
time_from_mock = fn mock_time ->
|
||||||
|
case String.length(mock_time) do
|
||||||
|
4 -> Time.from_iso8601!("T0#{mock_time}:00")
|
||||||
|
5 -> Time.from_iso8601!("T#{mock_time}:00")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
seconds_day = 86_400
|
||||||
|
seconds_days_14 = seconds_day * 14
|
||||||
|
seconds_half_day = Integer.floor_div(seconds_day, 2)
|
||||||
|
|
||||||
|
for user <- Accounts.list_users() do
|
||||||
|
user_shifts =
|
||||||
|
mock_shifts
|
||||||
|
|> Enum.take_random(:rand.uniform(15) + 5)
|
||||||
|
|> Enum.map(fn e ->
|
||||||
|
seconds_to_add = :rand.uniform(seconds_days_14) + seconds_half_day
|
||||||
|
add_dt = NaiveDateTime.add(user.inserted_at, seconds_to_add)
|
||||||
|
time_start = time_from_mock.(e["time_start"])
|
||||||
|
shift_len_min = e["length_minutes"] || 0
|
||||||
|
shift_length = e["length_hours"] * 60 * 60 + shift_len_min * 60
|
||||||
|
time_end = Time.add(time_start, shift_length) |> Time.truncate(:second)
|
||||||
|
|
||||||
|
%{
|
||||||
|
subject: e["subject"],
|
||||||
|
description: e["description"],
|
||||||
|
location: e["location"],
|
||||||
|
time_zone: Tzdata.zone_list() |> Enum.random(),
|
||||||
|
time_start: time_start,
|
||||||
|
time_end: time_end,
|
||||||
|
user_id: user.id,
|
||||||
|
inserted_at: add_dt,
|
||||||
|
updated_at: add_dt
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Repo.insert_all(ShiftTemplate, user_shifts)
|
||||||
|
end
|
||||||
|
|
||||||
|
#####
|
||||||
|
# insert shifts for each user?
|
||||||
|
alias Shift73k.Shifts
|
||||||
|
alias Shift73k.Shifts.Templates
|
||||||
|
|
||||||
|
for user <- Accounts.list_users() do
|
||||||
|
# build a date range for the time from 120 days ago to 120 days from now
|
||||||
|
today = Date.utc_today()
|
||||||
|
date_range = Date.range(Date.add(today, -120), Date.add(today, 120))
|
||||||
|
|
||||||
|
# get 3 random shift templates for user
|
||||||
|
st_list = Templates.list_shift_templates_by_user(user.id) |> Enum.take_random(3)
|
||||||
|
|
||||||
|
for st <- st_list do
|
||||||
|
days_to_schedule = Enum.take_random(date_range, 47)
|
||||||
|
shift_data = ShiftTemplate.attrs(st)
|
||||||
|
|
||||||
|
days_to_schedule
|
||||||
|
|> Stream.map(&Map.put(shift_data, :date, &1))
|
||||||
|
|> Enum.map(&Repo.timestamp/1)
|
||||||
|
|> Shifts.create_multiple()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
time_from_mock = fn mock_time ->
|
|
||||||
case String.length(mock_time) do
|
|
||||||
4 -> Time.from_iso8601!("T0#{mock_time}:00")
|
|
||||||
5 -> Time.from_iso8601!("T#{mock_time}:00")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
seconds_day = 86_400
|
|
||||||
seconds_days_14 = seconds_day * 14
|
|
||||||
seconds_half_day = Integer.floor_div(seconds_day, 2)
|
|
||||||
|
|
||||||
for user <- Accounts.list_users() do
|
|
||||||
user_shifts =
|
|
||||||
mock_shifts
|
|
||||||
|> Enum.take_random(:rand.uniform(15) + 5)
|
|
||||||
|> Enum.map(fn e ->
|
|
||||||
seconds_to_add = :rand.uniform(seconds_days_14) + seconds_half_day
|
|
||||||
add_dt = NaiveDateTime.add(user.inserted_at, seconds_to_add)
|
|
||||||
time_start = time_from_mock.(e["time_start"])
|
|
||||||
shift_len_min = e["length_minutes"] || 0
|
|
||||||
shift_length = e["length_hours"] * 60 * 60 + shift_len_min * 60
|
|
||||||
time_end = Time.add(time_start, shift_length) |> Time.truncate(:second)
|
|
||||||
|
|
||||||
%{
|
|
||||||
subject: e["subject"],
|
|
||||||
description: e["description"],
|
|
||||||
location: e["location"],
|
|
||||||
time_zone: Tzdata.zone_list() |> Enum.random(),
|
|
||||||
time_start: time_start,
|
|
||||||
time_end: time_end,
|
|
||||||
user_id: user.id,
|
|
||||||
inserted_at: add_dt,
|
|
||||||
updated_at: add_dt
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Repo.insert_all(ShiftTemplate, user_shifts)
|
|
||||||
end
|
|
||||||
|
|
||||||
#####
|
|
||||||
# insert shifts for each user?
|
|
||||||
alias Shift73k.Shifts
|
|
||||||
alias Shift73k.Shifts.Templates
|
|
||||||
|
|
||||||
for user <- Accounts.list_users() do
|
|
||||||
# build a date range for the time from 120 days ago to 120 days from now
|
|
||||||
today = Date.utc_today()
|
|
||||||
date_range = Date.range(Date.add(today, -120), Date.add(today, 120))
|
|
||||||
|
|
||||||
# get 3 random shift templates for user
|
|
||||||
st_list = Templates.list_shift_templates_by_user(user.id) |> Enum.take_random(3)
|
|
||||||
|
|
||||||
for st <- st_list do
|
|
||||||
days_to_schedule = Enum.take_random(date_range, 47)
|
|
||||||
shift_data = ShiftTemplate.attrs(st)
|
|
||||||
|
|
||||||
days_to_schedule
|
|
||||||
|> Stream.map(&Map.put(shift_data, :date, &1))
|
|
||||||
|> Enum.map(&Repo.timestamp/1)
|
|
||||||
|> Shifts.create_multiple()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
priv/static/android-chrome-192x192.png
Normal file
BIN
priv/static/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
BIN
priv/static/android-chrome-512x512.png
Normal file
BIN
priv/static/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue