Compare commits
No commits in common. "61796cf985781d55ee4fad2a28c3867f01e97149" and "24642d7c67a7c482a47830032ea46f5dddb8a280" have entirely different histories.
61796cf985
...
24642d7c67
14 changed files with 184 additions and 95 deletions
16
README.md
16
README.md
|
@ -6,19 +6,19 @@ Written in Elixir & Phoenix LiveView, with Bootstrap v5.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [X] ~~*Proper modal to delete shifts?*~~ [2022-08-14]
|
- [ ] Ability to edit shifts?
|
||||||
- [ ] Update tests, which are probably all way out of date. But I also don't care that much for this project...
|
- [ ] Proper modal to delete shifts?
|
||||||
|
- [ ] Allow all-day items for notes, or require hours even for sick days?
|
||||||
|
- [ ] Implement proper shift/template/assign tests (views etc)
|
||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
The below notes are old; I'm using a docker build to deploy this now. Will document when I have time.
|
|
||||||
|
|
||||||
### New versions
|
### New versions
|
||||||
|
|
||||||
When improvements are made, we can update the deployed version like so:
|
When improvements are made, we can update the deployed version like so:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd ${SHIFT73K_BASE_DIR}
|
cd /opt/shift73k
|
||||||
# update from master
|
# update from master
|
||||||
/usr/bin/git pull 73k master
|
/usr/bin/git pull 73k master
|
||||||
# fetch prod deps & compile
|
# fetch prod deps & compile
|
||||||
|
@ -27,10 +27,10 @@ MIX_ENV=prod /usr/bin/mix compile
|
||||||
# perform any migrations
|
# perform any migrations
|
||||||
MIX_ENV=prod /usr/bin/mix ecto.migrate
|
MIX_ENV=prod /usr/bin/mix ecto.migrate
|
||||||
# update node packages via package-lock.json
|
# update node packages via package-lock.json
|
||||||
/usr/bin/npm --prefix ./assets/ ci
|
/usr/bin/npm --prefix /opt/shift73k/assets/ ci
|
||||||
# rebuild static assets:
|
# rebuild static assets:
|
||||||
rm -rf ./priv/static/*
|
rm -rf /opt/shift73k/priv/static/*
|
||||||
/usr/bin/npm --prefix ./assets/ run build
|
/usr/bin/npm --prefix /opt/shift73k/assets/ run deploy
|
||||||
MIX_ENV=prod /usr/bin/mix phx.digest
|
MIX_ENV=prod /usr/bin/mix phx.digest
|
||||||
# rebuild release
|
# rebuild release
|
||||||
MIX_ENV=prod /usr/bin/mix release --overwrite
|
MIX_ENV=prod /usr/bin/mix release --overwrite
|
||||||
|
|
12
assets/package-lock.json
generated
12
assets/package-lock.json
generated
|
@ -57,9 +57,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.7.3",
|
"version": "18.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.2.tgz",
|
||||||
"integrity": "sha512-LJgzOEwWuMTBxHzgBR/fhhBOWrvBjvO+zPteUgbbuQi80rYIZHrk1mNbRUqPZqSLP2H7Rwt1EFLL/tNLD1Xx/w==",
|
"integrity": "sha512-ce7MIiaYWCFv6A83oEultwhBXb22fxwNOQf5DIxWA4WXvDQ7K+L0fbWl/YOfCzlR5B/uFkSnVBhPcOfOECcWvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/phoenix": {
|
"node_modules/@types/phoenix": {
|
||||||
|
@ -1070,9 +1070,9 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.7.3",
|
"version": "18.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.2.tgz",
|
||||||
"integrity": "sha512-LJgzOEwWuMTBxHzgBR/fhhBOWrvBjvO+zPteUgbbuQi80rYIZHrk1mNbRUqPZqSLP2H7Rwt1EFLL/tNLD1Xx/w==",
|
"integrity": "sha512-ce7MIiaYWCFv6A83oEultwhBXb22fxwNOQf5DIxWA4WXvDQ7K+L0fbWl/YOfCzlR5B/uFkSnVBhPcOfOECcWvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/phoenix": {
|
"@types/phoenix": {
|
||||||
|
|
83
config/runtime.exs
Normal file
83
config/runtime.exs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
# config/runtime.exs is executed for all environments, including
|
||||||
|
# during releases. It is executed after compilation and before the
|
||||||
|
# system starts, so it is typically used to load production configuration
|
||||||
|
# and secrets from environment variables or elsewhere. Do not define
|
||||||
|
# any compile-time configuration in here, as it won't be applied.
|
||||||
|
# The block below contains prod specific runtime configuration.
|
||||||
|
|
||||||
|
# ## Using releases
|
||||||
|
#
|
||||||
|
# If you use `mix release`, you need to explicitly enable the server
|
||||||
|
# by passing the PHX_SERVER=true when you start it:
|
||||||
|
#
|
||||||
|
# PHX_SERVER=true bin/shift73k start
|
||||||
|
#
|
||||||
|
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
||||||
|
# script that automatically sets the env var above.
|
||||||
|
if System.get_env("PHX_SERVER") do
|
||||||
|
config :shift73k, Shift73kWeb.Endpoint, server: true
|
||||||
|
end
|
||||||
|
|
||||||
|
if config_env() == :prod do
|
||||||
|
database_url =
|
||||||
|
System.get_env("DATABASE_URL") ||
|
||||||
|
raise """
|
||||||
|
environment variable DATABASE_URL is missing.
|
||||||
|
For example: ecto://USER:PASS@HOST/DATABASE
|
||||||
|
"""
|
||||||
|
|
||||||
|
maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
|
||||||
|
|
||||||
|
config :shift73k, Shift73k.Repo,
|
||||||
|
# ssl: true,
|
||||||
|
url: database_url,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||||
|
socket_options: maybe_ipv6
|
||||||
|
|
||||||
|
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||||
|
# A default value is used in config/dev.exs and config/test.exs but you
|
||||||
|
# want to use a different value for prod and you most likely don't want
|
||||||
|
# to check this value into version control, so we use an environment
|
||||||
|
# variable instead.
|
||||||
|
secret_key_base =
|
||||||
|
System.get_env("SECRET_KEY_BASE") ||
|
||||||
|
raise """
|
||||||
|
environment variable SECRET_KEY_BASE is missing.
|
||||||
|
You can generate one by calling: mix phx.gen.secret
|
||||||
|
"""
|
||||||
|
|
||||||
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
|
config :shift73k, Shift73kWeb.Endpoint,
|
||||||
|
url: [host: host, port: 443, scheme: "https"],
|
||||||
|
http: [
|
||||||
|
# Enable IPv6 and bind on all interfaces.
|
||||||
|
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||||
|
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
|
||||||
|
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
||||||
|
ip: {0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
port: port
|
||||||
|
],
|
||||||
|
secret_key_base: secret_key_base
|
||||||
|
|
||||||
|
# ## Configuring the mailer
|
||||||
|
#
|
||||||
|
# In production you need to configure the mailer to use a different adapter.
|
||||||
|
# Also, you may need to configure the Swoosh API client of your choice if you
|
||||||
|
# are not using SMTP. Here is an example of the configuration:
|
||||||
|
#
|
||||||
|
# config :shift73k, Shift73k.Mailer,
|
||||||
|
# adapter: Swoosh.Adapters.Mailgun,
|
||||||
|
# api_key: System.get_env("MAILGUN_API_KEY"),
|
||||||
|
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||||
|
#
|
||||||
|
# For this example you need include a HTTP client required by Swoosh API client.
|
||||||
|
# Swoosh supports Hackney and Finch out of the box:
|
||||||
|
#
|
||||||
|
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
||||||
|
#
|
||||||
|
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
||||||
|
end
|
|
@ -105,6 +105,9 @@ 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
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -18,6 +19,27 @@ 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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
<%= if @delete_days_shifts do %>
|
<%= if @delete_days_shifts do %>
|
||||||
<.live_component
|
<%= live_modal @socket, Shift73kWeb.ShiftAssignLive.DeleteComponent,
|
||||||
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 %>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
<%= if @delete_shift do %>
|
<%= if @delete_shift do %>
|
||||||
<.live_component
|
<%= live_modal @socket, Shift73kWeb.ShiftLive.DeleteComponent,
|
||||||
module={Shift73kWeb.ModalComponent}
|
|
||||||
id="modal"
|
|
||||||
component={Shift73kWeb.ShiftLive.DeleteComponent}
|
|
||||||
opts={[
|
|
||||||
id: @delete_shift.id,
|
id: @delete_shift.id,
|
||||||
title: "Delete Shift Template",
|
title: "Delete Shift Template",
|
||||||
delete_shift: @delete_shift
|
delete_shift: @delete_shift %>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,17 @@
|
||||||
<%= if @live_action in [:new, :edit, :clone] do %>
|
<%= if @live_action in [:new, :edit, :clone] do %>
|
||||||
<.live_component
|
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.FormComponent,
|
||||||
module={Shift73kWeb.ModalComponent}
|
|
||||||
id="modal"
|
|
||||||
component={Shift73kWeb.ShiftTemplateLive.FormComponent}
|
|
||||||
opts={[
|
|
||||||
id: @shift_template.id || :new,
|
id: @shift_template.id || :new,
|
||||||
title: @page_title,
|
title: @page_title,
|
||||||
action: @live_action,
|
action: @live_action,
|
||||||
shift_template: @shift_template,
|
shift_template: @shift_template,
|
||||||
current_user: @current_user
|
current_user: @current_user %>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @delete_shift_template do %>
|
<%= if @delete_shift_template do %>
|
||||||
<.live_component
|
<%= live_modal @socket, Shift73kWeb.ShiftTemplateLive.DeleteComponent,
|
||||||
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 %>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="row justify-content-center justify-content-md-start">
|
<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 @socket, 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 @socket, 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 @socket, 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} />
|
<%= live_component @socket, Shift73kWeb.UserLive.Settings.CalendarUrl, id: "calendar_url-#{@current_user.id}", current_user: @current_user %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,13 +9,9 @@ defmodule Shift73kWeb.UserManagement.DeleteComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("confirm", %{"id" => id, "email" => email} = params, socket) do
|
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
|
||||||
IO.inspect(params)
|
id
|
||||||
|
|> Accounts.get_user()
|
||||||
user = Accounts.get_user(id)
|
|
||||||
IO.inspect(user)
|
|
||||||
|
|
||||||
user
|
|
||||||
|> Accounts.delete_user()
|
|> Accounts.delete_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
|
|
|
@ -1,29 +1,18 @@
|
||||||
<%= if @live_action in [:new, :edit] do %>
|
<%= if @live_action in [:new, :edit] do %>
|
||||||
<.live_component
|
<%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent,
|
||||||
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_component
|
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
|
||||||
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 %>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
AND:
|
AND:
|
||||||
There are no users -- [REGISTER]
|
There are no users -- [REGISTER]
|
||||||
OR no registration allowed -- [LOG IN] %>
|
OR no registration allowed -- [LOG IN] %>
|
||||||
<% else %>
|
<%= else %>
|
||||||
|
|
||||||
<%= if !Repo.exists?(User) || allow_registration() do %>
|
<%= 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 %>
|
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "btn btn-outline-light") do %>
|
||||||
|
|
38
lib/shift73k_web/views/icon_helpers.ex
Normal file
38
lib/shift73k_web/views/icon_helpers.ex
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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,19 +0,0 @@
|
||||||
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
|
|
Loading…
Reference in a new issue