diff --git a/lib/chore_tracker/chores.ex b/lib/chore_tracker/chores.ex index 905c918..6a26587 100644 --- a/lib/chore_tracker/chores.ex +++ b/lib/chore_tracker/chores.ex @@ -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 diff --git a/lib/chore_tracker/chores/chore.ex b/lib/chore_tracker/chores/chore.ex index 53ebcc1..b6a44ae 100644 --- a/lib/chore_tracker/chores/chore.ex +++ b/lib/chore_tracker/chores/chore.ex @@ -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 diff --git a/lib/chore_tracker_web/live/chore_live/show.ex b/lib/chore_tracker_web/live/chore_live/show.ex index f53553a..f5b565a 100644 --- a/lib/chore_tracker_web/live/chore_live/show.ex +++ b/lib/chore_tracker_web/live/chore_live/show.ex @@ -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 diff --git a/lib/chore_tracker_web/live/chore_live/show.html.heex b/lib/chore_tracker_web/live/chore_live/show.html.heex index ae3d7be..8e64da6 100644 --- a/lib/chore_tracker_web/live/chore_live/show.html.heex +++ b/lib/chore_tracker_web/live/chore_live/show.html.heex @@ -59,13 +59,22 @@
-

Next assignee

- - <%= if next_assignee = Chores.next_chore_assignee(@chore) do %> -

<%= Accounts.display_user(next_assignee) %>

- <% else %> -

None

- <% end %> + + <.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 +
diff --git a/lib/chore_tracker_web/live/overview_live.ex b/lib/chore_tracker_web/live/overview_live.ex index 2e33a4c..aff736e 100644 --- a/lib/chore_tracker_web/live/overview_live.ex +++ b/lib/chore_tracker_web/live/overview_live.ex @@ -36,7 +36,7 @@ defmodule ChoreTrackerWeb.OverviewLive do

<%= display_relative(next_date) %>

<: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 %> diff --git a/priv/repo/migrations/20240807222134_create_chores.exs b/priv/repo/migrations/20240807222134_create_chores.exs index 07397ed..5b788e2 100644 --- a/priv/repo/migrations/20240807222134_create_chores.exs +++ b/priv/repo/migrations/20240807222134_create_chores.exs @@ -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