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 def list_chores do
logs = from(log in ChoreLog, limit: 1) 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 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 def create_chore(attrs \\ %{}) do
%Chore{} %Chore{}
@ -24,9 +25,12 @@ defmodule ChoreTracker.Chores do
end end
def update_chore(%Chore{} = chore, attrs) do def update_chore(%Chore{} = chore, attrs) do
chore Ecto.Multi.new()
|> change_chore(attrs) |> Ecto.Multi.update(:chore, change_chore(chore, attrs))
|> Repo.update() |> Ecto.Multi.run(:update_next_assignee, fn _repo, _changeset ->
save_next_chore_assignee(chore)
end)
|> Repo.transaction()
end end
def delete_chore(%Chore{} = chore) do def delete_chore(%Chore{} = chore) do
@ -41,6 +45,14 @@ defmodule ChoreTracker.Chores do
|> Changeset.put_assoc(:assignees, assignees) |> Changeset.put_assoc(:assignees, assignees)
end 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 def get_last_chore_log_for_assignee(%Chore{} = chore, %User{} = user) do
from(log in ChoreLog, from(log in ChoreLog,
where: log.chore_id == ^chore.id and log.user_id == ^user.id, where: log.chore_id == ^chore.id and log.user_id == ^user.id,
@ -51,9 +63,15 @@ defmodule ChoreTracker.Chores do
end end
def log_chore_execution(%User{} = user, %Chore{} = chore) do def log_chore_execution(%User{} = user, %Chore{} = chore) do
%ChoreLog{} Ecto.Multi.new()
|> ChoreLog.changeset(%{user_id: user.id, chore_id: chore.id}) |> Ecto.Multi.insert(
|> Repo.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 end
def update_chore_log(%ChoreLog{} = chore_log, attrs) do def update_chore_log(%ChoreLog{} = chore_log, attrs) do
@ -83,6 +101,11 @@ defmodule ChoreTracker.Chores do
chore.starts_at chore.starts_at
end 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 def next_chore_assignee(%Chore{} = chore) do
chore.assignees chore.assignees
|> Enum.map(&{&1, get_last_chore_log_for_assignee(chore, &1)}) |> 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) |> Enum.map(fn {assignee, _log} -> assignee end)
|> List.first() |> List.first()
end 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 end

View file

@ -1,6 +1,8 @@
defmodule ChoreTracker.Chores.Chore do defmodule ChoreTracker.Chores.Chore do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias ChoreTracker.Chores
alias ChoreTracker.Accounts
schema "chores" do schema "chores" do
field :name, :string field :name, :string
@ -12,9 +14,11 @@ defmodule ChoreTracker.Chores.Chore do
timestamps(type: :utc_datetime) 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", join_through: "chore_assignees",
on_replace: :delete on_replace: :delete
end end
@ -22,7 +26,15 @@ defmodule ChoreTracker.Chores.Chore do
@doc false @doc false
def changeset(chore, attrs) do def changeset(chore, attrs) do
chore 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_required([:name, :emoji, :period, :period_unit, :starts_at])
|> validate_length(:emoji, min: 1, max: 1) |> validate_length(:emoji, min: 1, max: 1)
end end

View file

@ -1,4 +1,5 @@
defmodule ChoreTrackerWeb.ChoreLive.Show do defmodule ChoreTrackerWeb.ChoreLive.Show do
alias ChoreTracker.Repo
use ChoreTrackerWeb, :live_view use ChoreTrackerWeb, :live_view
alias ChoreTracker.Chores alias ChoreTracker.Chores
@ -11,15 +12,18 @@ defmodule ChoreTrackerWeb.ChoreLive.Show do
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id}, _, socket) do
chore = Chores.get_chore!(id)
{:noreply, {:noreply,
socket socket
|> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:chore, Chores.get_chore!(id))} |> update_state(chore)}
end end
defp page_title(:show), do: "Show Chore" defp page_title(:show), do: "Show Chore"
defp page_title(:edit), do: "Edit Chore" defp page_title(:edit), do: "Edit Chore"
@impl true
def handle_event("log_execution", _params, socket) do def handle_event("log_execution", _params, socket) do
%{current_user: user, chore: chore} = socket.assigns %{current_user: user, chore: chore} = socket.assigns
@ -28,7 +32,31 @@ defmodule ChoreTrackerWeb.ChoreLive.Show do
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "Chore execution logged.") |> put_flash(:info, "Chore execution logged.")
|> assign(:chore, Chores.get_chore!(chore.id))} |> update_state(Chores.get_chore!(chore.id))}
end end
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 end

View file

@ -59,13 +59,22 @@
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<p class="text-sm font-medium text-zinc-500 mb-2">Next assignee</p> <label for="chore_next_assignee_id" class="text-sm font-medium text-zinc-500">
Next assignee
<%= if next_assignee = Chores.next_chore_assignee(@chore) do %> </label>
<p><%= Accounts.display_user(next_assignee) %></p> <.form
<% else %> for={@assignee_form}
<p>None</p> class="flex flex-wrap gap-2 items-end"
<% 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>
<div class="col-span-12"> <div class="col-span-12">

View file

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

View file

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