Compare commits

..

No commits in common. "61796cf985781d55ee4fad2a28c3867f01e97149" and "24642d7c67a7c482a47830032ea46f5dddb8a280" have entirely different histories.

14 changed files with 184 additions and 95 deletions

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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
""" """

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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>

View file

@ -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, _} ->

View file

@ -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 %>

View file

@ -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 %>

View 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

View file

@ -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