Make next assignee reassignable

This commit is contained in:
Ondřej 2024-08-09 14:24:26 +02:00
parent 3b86eeaa23
commit f9b0847b7f
6 changed files with 104 additions and 21 deletions

View file

@ -12,10 +12,11 @@ defmodule ChoreTracker.Chores do
def list_chores do
logs = from(log in ChoreLog, limit: 1)
from(chore in Chore, preload: [:assignees, logs: ^logs]) |> Repo.all()
from(chore in Chore, preload: [:assignees, :next_assignee, logs: ^logs]) |> Repo.all()
end
def get_chore!(id), do: Repo.get!(Chore, id) |> Repo.preload([{:logs, :user}, :assignees])
def get_chore!(id),
do: Repo.get!(Chore, id) |> Repo.preload([{:logs, :user}, :assignees, :next_assignee])
def create_chore(attrs \\ %{}) do
%Chore{}
@ -24,9 +25,12 @@ defmodule ChoreTracker.Chores do
end
def update_chore(%Chore{} = chore, attrs) do
chore
|> change_chore(attrs)
|> Repo.update()
Ecto.Multi.new()
|> Ecto.Multi.update(:chore, change_chore(chore, attrs))
|> Ecto.Multi.run(:update_next_assignee, fn _repo, _changeset ->
save_next_chore_assignee(chore)
end)
|> Repo.transaction()
end
def delete_chore(%Chore{} = chore) do
@ -41,6 +45,14 @@ defmodule ChoreTracker.Chores do
|> Changeset.put_assoc(:assignees, assignees)
end
def update_chore_assignee(%Chore{} = chore, attrs) do
change_chore_assignee(chore, attrs) |> Repo.update()
end
def change_chore_assignee(%Chore{} = chore, attrs \\ %{}) do
chore |> Ecto.Changeset.cast(attrs, [:next_assignee_id])
end
def get_last_chore_log_for_assignee(%Chore{} = chore, %User{} = user) do
from(log in ChoreLog,
where: log.chore_id == ^chore.id and log.user_id == ^user.id,
@ -51,9 +63,15 @@ defmodule ChoreTracker.Chores do
end
def log_chore_execution(%User{} = user, %Chore{} = chore) do
%ChoreLog{}
|> ChoreLog.changeset(%{user_id: user.id, chore_id: chore.id})
|> Repo.insert()
Ecto.Multi.new()
|> Ecto.Multi.insert(
:log,
%ChoreLog{} |> ChoreLog.changeset(%{user_id: user.id, chore_id: chore.id})
)
|> Ecto.Multi.run(:chore, fn _repo, _change ->
save_next_chore_assignee(chore)
end)
|> Repo.transaction()
end
def update_chore_log(%ChoreLog{} = chore_log, attrs) do
@ -83,6 +101,11 @@ defmodule ChoreTracker.Chores do
chore.starts_at
end
@doc """
Calculate next assignee for the chore.
Assigns the person who last did the task the longest time ago.
Returns `nil` if chore has no assignees.
"""
def next_chore_assignee(%Chore{} = chore) do
chore.assignees
|> Enum.map(&{&1, get_last_chore_log_for_assignee(chore, &1)})
@ -98,4 +121,14 @@ defmodule ChoreTracker.Chores do
|> Enum.map(fn {assignee, _log} -> assignee end)
|> List.first()
end
defp save_next_chore_assignee(%Chore{} = chore) do
next_assignee = next_chore_assignee(chore)
if next_assignee do
chore |> update_chore_assignee(%{next_assignee_id: next_assignee.id})
else
{:ok, nil}
end
end
end

View file

@ -1,6 +1,8 @@
defmodule ChoreTracker.Chores.Chore do
use Ecto.Schema
import Ecto.Changeset
alias ChoreTracker.Chores
alias ChoreTracker.Accounts
schema "chores" do
field :name, :string
@ -12,9 +14,11 @@ defmodule ChoreTracker.Chores.Chore do
timestamps(type: :utc_datetime)
has_many :logs, ChoreTracker.Chores.ChoreLog, preload_order: [desc: :inserted_at]
has_many :logs, Chores.ChoreLog, preload_order: [desc: :inserted_at]
many_to_many :assignees, ChoreTracker.Accounts.User,
belongs_to :next_assignee, Accounts.User
many_to_many :assignees, Accounts.User,
join_through: "chore_assignees",
on_replace: :delete
end
@ -22,7 +26,15 @@ defmodule ChoreTracker.Chores.Chore do
@doc false
def changeset(chore, attrs) do
chore
|> cast(attrs, [:name, :description, :emoji, :period, :period_unit, :starts_at])
|> cast(attrs, [
:name,
:description,
:emoji,
:period,
:period_unit,
:starts_at,
:next_assignee_id
])
|> validate_required([:name, :emoji, :period, :period_unit, :starts_at])
|> validate_length(:emoji, min: 1, max: 1)
end

View file

@ -1,4 +1,5 @@
defmodule ChoreTrackerWeb.ChoreLive.Show do
alias ChoreTracker.Repo
use ChoreTrackerWeb, :live_view
alias ChoreTracker.Chores
@ -11,15 +12,18 @@ defmodule ChoreTrackerWeb.ChoreLive.Show do
@impl true
def handle_params(%{"id" => id}, _, socket) do
chore = Chores.get_chore!(id)
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:chore, Chores.get_chore!(id))}
|> update_state(chore)}
end
defp page_title(:show), do: "Show Chore"
defp page_title(:edit), do: "Edit Chore"
@impl true
def handle_event("log_execution", _params, socket) do
%{current_user: user, chore: chore} = socket.assigns
@ -28,7 +32,31 @@ defmodule ChoreTrackerWeb.ChoreLive.Show do
{:noreply,
socket
|> put_flash(:info, "Chore execution logged.")
|> assign(:chore, Chores.get_chore!(chore.id))}
|> update_state(Chores.get_chore!(chore.id))}
end
end
@impl true
def handle_event("change_assignee", params, socket) do
%{"chore" => chore_params} = params
chore = socket.assigns.chore
case Chores.update_chore_assignee(chore, chore_params) do
{:ok, chore} ->
{:noreply,
socket
|> put_flash(:info, "Assignee changed")
|> update_state(chore)}
end
end
def assignee_options() do
Accounts.list_users() |> Enum.map(&{Accounts.display_user(&1), &1.id})
end
defp update_state(socket, chore) do
socket
|> assign(:chore, chore |> Repo.preload(:next_assignee))
|> assign(:assignee_form, to_form(Chores.change_chore_assignee(chore)))
end
end

View file

@ -59,13 +59,22 @@
</div>
<div class="col-span-6">
<p class="text-sm font-medium text-zinc-500 mb-2">Next assignee</p>
<%= if next_assignee = Chores.next_chore_assignee(@chore) do %>
<p><%= Accounts.display_user(next_assignee) %></p>
<% else %>
<p>None</p>
<% end %>
<label for="chore_next_assignee_id" class="text-sm font-medium text-zinc-500">
Next assignee
</label>
<.form
for={@assignee_form}
class="flex flex-wrap gap-2 items-end"
phx-submit="change_assignee"
>
<.input
field={@assignee_form[:next_assignee_id]}
prompt="None"
type="select"
options={assignee_options()}
/>
<.button type="submit">Save</.button>
</.form>
</div>
<div class="col-span-12">

View file

@ -36,7 +36,7 @@ defmodule ChoreTrackerWeb.OverviewLive do
<p><%= display_relative(next_date) %></p>
</:col>
<:col :let={chore} label="Next assignee">
<%= case Chores.next_chore_assignee(chore) do
<%= case chore.next_assignee do
nil -> "No assignee"
%Accounts.User{} = user -> Accounts.display_user(user)
end %>

View file

@ -9,6 +9,7 @@ defmodule ChoreTracker.Repo.Migrations.CreateChores do
add :period, :integer
add :period_unit, :string
add :starts_at, :date
add :next_assignee_id, references(:users, on_delete: :nilify_all)
timestamps(type: :utc_datetime)
end