parent
177067ab00
commit
9af0124138
11 changed files with 608 additions and 60 deletions
145
assets/js/app.js
145
assets/js/app.js
|
@ -18,8 +18,135 @@ import "phoenix_html"
|
||||||
import {Socket} from "phoenix"
|
import {Socket} from "phoenix"
|
||||||
import LiveSocket from "phoenix_live_view"
|
import LiveSocket from "phoenix_live_view"
|
||||||
|
|
||||||
|
let Hooks = {}
|
||||||
|
|
||||||
|
Hooks.Dragger = {
|
||||||
|
toggleAddDelete() {
|
||||||
|
const deleter = document.getElementById('deleter')
|
||||||
|
if (deleter) {
|
||||||
|
const adder = document.getElementById('adder')
|
||||||
|
deleter.hidden = adder.hidden
|
||||||
|
adder.hidden = !adder.hidden
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get dragImage() {
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
canvas.width = canvas.height = 60
|
||||||
|
const ctx = canvas.getContext("2d")
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(30, 30, 30, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.el.addEventListener("dragstart", event => {
|
||||||
|
event.dataTransfer.setData("text/plain", `task-id:${this.el.dataset.taskId}`)
|
||||||
|
event.dataTransfer.setDragImage(this.dragImage, 25, 25)
|
||||||
|
this.toggleAddDelete()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener("dragend", event => {
|
||||||
|
this.toggleAddDelete()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Hooks.AddDropper = {
|
||||||
|
get bgClass() { return "has-background-warning"},
|
||||||
|
|
||||||
|
addTaskToPlan(payload) { return this.pushEvent("add-task-to-plan", payload) },
|
||||||
|
|
||||||
|
getTaskPayload(event) { return event.dataTransfer.getData("text/plain") },
|
||||||
|
|
||||||
|
parseTaskPayload(payload) { return payload.startsWith("task-id:") ? payload.split(":")[1] : null },
|
||||||
|
|
||||||
|
addHoverClass(event) { event.target.classList.add(this.bgClass) },
|
||||||
|
|
||||||
|
removeHoverClass(event) { event.target.classList.remove(this.bgClass) },
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.el.addEventListener("drop", event => {
|
||||||
|
event.preventDefault()
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) {
|
||||||
|
const planID = this.el.dataset.planId
|
||||||
|
this.addTaskToPlan({ "task_id": taskID, "plan_id": planID, })
|
||||||
|
this.removeHoverClass(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener("dragover", event => event.preventDefault())
|
||||||
|
|
||||||
|
this.el.addEventListener("dragenter", event => {
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) { this.addHoverClass(event) }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener("dragleave", event => {
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) { this.removeHoverClass(event) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Hooks.DeleteDropper = {
|
||||||
|
get hoverBGClass() { return "has-background-warning"},
|
||||||
|
|
||||||
|
get baseBGClass() { return "has-background-danger"},
|
||||||
|
|
||||||
|
deleteTaskFromPlan(payload) { return this.pushEvent("delete-task-from-plan", payload) },
|
||||||
|
|
||||||
|
getTaskPayload(event) { return event.dataTransfer.getData("text/plain") },
|
||||||
|
|
||||||
|
parseTaskPayload(payload) { return payload.startsWith("task-id:") ? payload.split(":")[1] : null },
|
||||||
|
|
||||||
|
addHoverClass(event) {
|
||||||
|
event.target.classList.add(this.hoverBGClass)
|
||||||
|
event.target.classList.remove(this.baseBGClass)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeHoverClass(event) {
|
||||||
|
event.target.classList.remove(this.hoverBGClass)
|
||||||
|
event.target.classList.add(this.baseBGClass)
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.el.addEventListener("drop", event => {
|
||||||
|
event.preventDefault()
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) {
|
||||||
|
const planID = this.el.dataset.drop
|
||||||
|
this.deleteTaskFromPlan({ "task_id": taskID, "plan_id": planID, })
|
||||||
|
this.removeHoverClass(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener("dragover", event => event.preventDefault())
|
||||||
|
|
||||||
|
this.el.addEventListener("dragenter", event => {
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) { this.addHoverClass(event) }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener("dragleave", event => {
|
||||||
|
const payload = this.getTaskPayload(event)
|
||||||
|
const taskID = this.parseTaskPayload(payload)
|
||||||
|
if (taskID !== null) { this.removeHoverClass(event) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||||
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
|
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})
|
||||||
|
|
||||||
// Connect if there are any LiveViews on the page
|
// Connect if there are any LiveViews on the page
|
||||||
liveSocket.connect()
|
liveSocket.connect()
|
||||||
|
@ -33,15 +160,15 @@ liveSocket.connect()
|
||||||
window.liveSocket = liveSocket
|
window.liveSocket = liveSocket
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0)
|
||||||
if ($navbarBurgers.length > 0) {
|
if ($navbarBurgers.length > 0) {
|
||||||
$navbarBurgers.forEach( el => {
|
$navbarBurgers.forEach( el => {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
const target = el.dataset.target;
|
const target = el.dataset.target
|
||||||
const $target = document.getElementById(target);
|
const $target = document.getElementById(target)
|
||||||
el.classList.toggle('is-active');
|
el.classList.toggle('is-active')
|
||||||
$target.classList.toggle('is-active');
|
$target.classList.toggle('is-active')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
defmodule Planner.Tasks do
|
defmodule Planner.Tasks do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Ecto.Multi
|
||||||
alias Ecto.UUID
|
alias Ecto.UUID
|
||||||
alias Planner.Repo
|
alias Planner.Repo
|
||||||
alias Planner.Tasks.Task
|
alias Planner.Tasks.Task
|
||||||
|
alias Planner.Tasks.Plan
|
||||||
|
alias Planner.Tasks.PlanDetail
|
||||||
|
|
||||||
def list_all_tasks, do: Repo.all(Task)
|
def list_all_tasks, do: Repo.all(Task)
|
||||||
|
|
||||||
|
@ -15,6 +19,20 @@ defmodule Planner.Tasks do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_unfinished_tasks_by_plan_id(plan_id) do
|
||||||
|
q =
|
||||||
|
Ecto.Query.from(
|
||||||
|
t in Task,
|
||||||
|
join: pd in PlanDetail,
|
||||||
|
on: t.id == pd.task_id,
|
||||||
|
where: pd.plan_id == ^plan_id and is_nil(t.finished_at),
|
||||||
|
order_by: [desc: t.updated_at]
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.all(q)
|
||||||
|
|> Repo.preload(:plans)
|
||||||
|
end
|
||||||
|
|
||||||
def list_finished_tasks do
|
def list_finished_tasks do
|
||||||
from(
|
from(
|
||||||
t in Task,
|
t in Task,
|
||||||
|
@ -32,6 +50,15 @@ defmodule Planner.Tasks do
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_task_and_add_to_plan(task_attrs, plan) do
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.insert(:task, Task.changeset(%Task{}, task_attrs))
|
||||||
|
|> Multi.run(:plan_detail, fn _repo, %{task: task} ->
|
||||||
|
create_plan_detail(%{"task_id" => task.id, "plan_id" => plan.id})
|
||||||
|
end)
|
||||||
|
|> Repo.transaction()
|
||||||
|
end
|
||||||
|
|
||||||
def update_task(%Task{} = task, attrs) do
|
def update_task(%Task{} = task, attrs) do
|
||||||
task
|
task
|
||||||
|> Task.changeset(attrs)
|
|> Task.changeset(attrs)
|
||||||
|
@ -69,4 +96,89 @@ defmodule Planner.Tasks do
|
||||||
_ -> task_exists?(task_id)
|
_ -> task_exists?(task_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_plans do
|
||||||
|
Repo.all(Plan)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_unfinished_plans do
|
||||||
|
from(
|
||||||
|
p in Plan,
|
||||||
|
where: is_nil(p.finished_at),
|
||||||
|
order_by: [desc: p.updated_at]
|
||||||
|
)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_plan!(id), do: Repo.get!(Plan, id)
|
||||||
|
|
||||||
|
def create_plan(attrs \\ %{}) do
|
||||||
|
%Plan{}
|
||||||
|
|> Plan.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_plan(%Plan{} = plan, attrs) do
|
||||||
|
plan
|
||||||
|
|> Plan.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_plan(%Plan{} = plan) do
|
||||||
|
Repo.delete(plan)
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_plan(%Plan{} = plan, attrs \\ %{}) do
|
||||||
|
Plan.changeset(plan, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def plan_exists?(id), do: Repo.exists?(from(p in Plan, where: p.id == ^id))
|
||||||
|
|
||||||
|
def finish_plan_by_id!(id) do
|
||||||
|
get_plan!(id)
|
||||||
|
|> Plan.finish_plan()
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_plan_id_from_url(plan_id) do
|
||||||
|
plan_id =
|
||||||
|
case UUID.dump(plan_id) do
|
||||||
|
# don't actually want the dumped UUID, so discard
|
||||||
|
{:ok, _} -> plan_id
|
||||||
|
:error -> :error
|
||||||
|
end
|
||||||
|
|
||||||
|
case plan_id do
|
||||||
|
:error -> :error
|
||||||
|
_ -> plan_exists?(plan_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_plan_details do
|
||||||
|
Repo.all(PlanDetail)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_plan_detail!(id), do: Repo.get!(PlanDetail, id)
|
||||||
|
|
||||||
|
def get_plan_detail_by!(clauses), do: Repo.get_by!(PlanDetail, clauses)
|
||||||
|
|
||||||
|
def create_plan_detail(attrs \\ %{}, on_conflict \\ :nothing) do
|
||||||
|
%PlanDetail{}
|
||||||
|
|> PlanDetail.changeset(attrs)
|
||||||
|
|> Repo.insert(on_conflict: on_conflict)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_plan_detail(%PlanDetail{} = plan_detail, attrs) do
|
||||||
|
plan_detail
|
||||||
|
|> PlanDetail.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_plan_detail(%PlanDetail{} = plan_detail) do
|
||||||
|
Repo.delete(plan_detail)
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_plan_detail(%PlanDetail{} = plan_detail, attrs \\ %{}) do
|
||||||
|
PlanDetail.changeset(plan_detail, attrs)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
29
lib/planner/tasks/plan.ex
Normal file
29
lib/planner/tasks/plan.ex
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule Planner.Tasks.Plan do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
schema "plans" do
|
||||||
|
field :description, :string
|
||||||
|
field :finished_at, :naive_datetime
|
||||||
|
field :end, :naive_datetime
|
||||||
|
field :name, :string
|
||||||
|
field :start, :naive_datetime
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(plan, attrs) do
|
||||||
|
plan
|
||||||
|
|> cast(attrs, [:description, :finished_at, :start, :end, :name])
|
||||||
|
|> validate_required([:name])
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish_plan(plan) do
|
||||||
|
# TODO, this should check if `finished_at` is not nil, first
|
||||||
|
change(plan, finished_at: now())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp now(), do: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||||
|
end
|
19
lib/planner/tasks/plan_detail.ex
Normal file
19
lib/planner/tasks/plan_detail.ex
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Planner.Tasks.PlanDetail do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
schema "plan_details" do
|
||||||
|
field :sort, :integer
|
||||||
|
field :task_id, :binary_id
|
||||||
|
field :plan_id, :binary_id
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(plan_detail, attrs) do
|
||||||
|
plan_detail
|
||||||
|
|> cast(attrs, [:sort, :task_id, :plan_id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,8 @@ defmodule Planner.Tasks.Task do
|
||||||
field(:finished_at, :naive_datetime)
|
field(:finished_at, :naive_datetime)
|
||||||
field(:due_at, :naive_datetime)
|
field(:due_at, :naive_datetime)
|
||||||
|
|
||||||
|
many_to_many(:plans, Planner.Tasks.Plan, join_through: "plan_details", on_delete: :delete_all)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ defmodule TasksComponent do
|
||||||
~L"""
|
~L"""
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<%= f = form_for(@changeset, "#", [phx_submit: "new-task"]) %>
|
<%= f = form_for(@changeset, "#", [phx_submit: "new-task"]) %>
|
||||||
<div class="field">
|
<div id="adder" class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<%= text_input(f,
|
<%= text_input(f,
|
||||||
:value,
|
:value,
|
||||||
|
@ -33,6 +33,17 @@ defmodule TasksComponent do
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<%= if(!is_nil(@active_plan)) do %>
|
||||||
|
<div
|
||||||
|
id="deleter"
|
||||||
|
phx-hook="DeleteDropper"
|
||||||
|
data-drop="<%= @active_plan.id %>"
|
||||||
|
class="has-background-danger"
|
||||||
|
style="height: 38px; width: 100%"
|
||||||
|
hidden=true
|
||||||
|
></div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<ul class="tasks">
|
<ul class="tasks">
|
||||||
<%= for task <- @tasks do %>
|
<%= for task <- @tasks do %>
|
||||||
<%= live_component(@socket,
|
<%= live_component(@socket,
|
||||||
|
@ -73,7 +84,7 @@ defmodule TaskComponent do
|
||||||
<div class="ml-5-5">
|
<div class="ml-5-5">
|
||||||
<%= if(@is_active) do %>
|
<%= if(@is_active) do %>
|
||||||
<%= case @live_action do %>
|
<%= case @live_action do %>
|
||||||
<% :show -> %>
|
<% :show_task -> %>
|
||||||
<%= live_component(@socket,
|
<%= live_component(@socket,
|
||||||
TaskDetailsComponent,
|
TaskDetailsComponent,
|
||||||
id: "task_details:#{@task.id}",
|
id: "task_details:#{@task.id}",
|
||||||
|
@ -81,7 +92,7 @@ defmodule TaskComponent do
|
||||||
route_index_tasks: @route_index_tasks,
|
route_index_tasks: @route_index_tasks,
|
||||||
route_edit_task: @route_edit_task
|
route_edit_task: @route_edit_task
|
||||||
)%>
|
)%>
|
||||||
<% :edit -> %>
|
<% :edit_task -> %>
|
||||||
<%= live_component(@socket,
|
<%= live_component(@socket,
|
||||||
TaskEditComponent,
|
TaskEditComponent,
|
||||||
id: "task_edit:#{@task.id}",
|
id: "task_edit:#{@task.id}",
|
||||||
|
@ -116,7 +127,7 @@ defmodule TaskDetailsComponent do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<div class="box">
|
<div class="box" id="task-details-<%= @task.id %>" draggable="true" phx-hook="Dragger" data-task-id="<%= @task.id %>">
|
||||||
<%= live_patch("",
|
<%= live_patch("",
|
||||||
to: @route_index_tasks.(@socket),
|
to: @route_index_tasks.(@socket),
|
||||||
class: "delete is-pulled-right"
|
class: "delete is-pulled-right"
|
||||||
|
|
|
@ -2,48 +2,165 @@ defmodule PlannerWeb.TasksLive do
|
||||||
use PlannerWeb, :live_view
|
use PlannerWeb, :live_view
|
||||||
|
|
||||||
alias Planner.Tasks
|
alias Planner.Tasks
|
||||||
|
alias Planner.Tasks.Plan
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:tasks, Tasks.list_unfinished_tasks())
|
|> assign(:plans, Tasks.list_unfinished_plans())
|
||||||
|> assign(:active_task, nil)
|
|> assign(:plan_changeset, Tasks.change_plan(%Plan{}))
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(%{"task_id" => task_id}, _, socket) do
|
# plan: yes, task: yes
|
||||||
case Tasks.verify_task_id_from_url(task_id) do
|
def handle_params(%{"plan_id" => plan_id, "task_id" => task_id}, _, socket) do
|
||||||
true -> {:noreply, assign(socket, :active_task, task_id)}
|
case Tasks.verify_plan_id_from_url(plan_id) and Tasks.verify_task_id_from_url(task_id) do
|
||||||
_ -> {:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
true ->
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:active_task, task_id)
|
||||||
|
|> assign(:active_plan, Tasks.get_plan!(plan_id))
|
||||||
|
|> assign(:tasks, Tasks.list_unfinished_tasks_by_plan_id(plan_id))
|
||||||
|
|> add_plan_routes(plan_id)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# plan: no, task: yes
|
||||||
|
def handle_params(%{"task_id" => task_id}, _, socket) do
|
||||||
|
case Tasks.verify_task_id_from_url(task_id) do
|
||||||
|
true ->
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:active_task, task_id)
|
||||||
|
|> assign(:active_plan, nil)
|
||||||
|
|> assign(:tasks, Tasks.list_unfinished_tasks())
|
||||||
|
|> add_task_routes()
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :active_task, task_id)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# plan: yes, task: no
|
||||||
|
def handle_params(%{"plan_id" => plan_id}, _, socket) do
|
||||||
|
case Tasks.verify_plan_id_from_url(plan_id) do
|
||||||
|
true ->
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:active_task, nil)
|
||||||
|
|> assign(:active_plan, Tasks.get_plan!(plan_id))
|
||||||
|
|> assign(:tasks, Tasks.list_unfinished_tasks_by_plan_id(plan_id))
|
||||||
|
|> add_plan_routes(plan_id)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# plan: no, task: no
|
||||||
def handle_params(_, _, socket) do
|
def handle_params(_, _, socket) do
|
||||||
{:noreply, assign(socket, :active_task, nil)}
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:active_task, nil)
|
||||||
|
|> assign(:active_plan, nil)
|
||||||
|
|> assign(:tasks, Tasks.list_unfinished_tasks())
|
||||||
|
|> add_task_routes()
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<div phx-window-keydown="keydown" phx-key="Escape">
|
<div class="columns" phx-window-keydown="keydown" phx-key="Escape">
|
||||||
|
<div class="column is-one-quarter">
|
||||||
|
<h4 class="title is-4">plans</h4>
|
||||||
|
<nav class="panel">
|
||||||
|
<%= f = form_for(@plan_changeset, "#", phx_submit: "new-plan", class: "panel-block") %>
|
||||||
|
<div class="control">
|
||||||
|
<%= text_input(f,
|
||||||
|
:name,
|
||||||
|
placeholder: "add new plan",
|
||||||
|
class: "input", autocomplete: "off"
|
||||||
|
)%>
|
||||||
|
<%= error_tag(f, :name) %>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<%= live_patch("all unfinished tasks", to: Routes.tasks_path(@socket, :index), class: "panel-block") %>
|
||||||
|
<%= for plan <- @plans do %>
|
||||||
|
<%= live_patch(
|
||||||
|
plan.name,
|
||||||
|
to: Routes.tasks_path(@socket, :show_plan, plan.id),
|
||||||
|
class: "panel-block",
|
||||||
|
phx_hook: "AddDropper",
|
||||||
|
data_plan_id: plan.id
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<%= case @active_plan do %>
|
||||||
|
<%= nil -> %>
|
||||||
|
<h4 class="title is-4">all unfinished tasks</h4>
|
||||||
|
<% _ -> %>
|
||||||
|
<h4 class="title is-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="checkbox"
|
||||||
|
class="doit"
|
||||||
|
phx-click="finish-plan"
|
||||||
|
phx-value-plan-id="<%= @active_plan.id %>">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<%= @active_plan.name %>
|
||||||
|
</h4>
|
||||||
|
<% end %>
|
||||||
<%= live_component(@socket,
|
<%= live_component(@socket,
|
||||||
TasksComponent,
|
TasksComponent,
|
||||||
id: :all_unfinished_tasks,
|
id: :tasks,
|
||||||
live_action: @live_action,
|
live_action: @live_action,
|
||||||
tasks: @tasks,
|
tasks: @tasks,
|
||||||
|
active_plan: @active_plan,
|
||||||
active_task: @active_task,
|
active_task: @active_task,
|
||||||
route_show_task: &(Routes.tasks_path(&1, :show, &2)),
|
route_show_task: @route_show_task,
|
||||||
route_edit_task: &(Routes.tasks_path(&1, :edit, &2)),
|
route_edit_task: @route_edit_task,
|
||||||
route_index_tasks: &(Routes.tasks_path(&1, :index))
|
route_index_tasks: @route_index_tasks
|
||||||
)%>
|
)%>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("new-plan", %{"plan" => plan_params}, socket) do
|
||||||
|
case Tasks.create_plan(plan_params) do
|
||||||
|
{:ok, _plan} ->
|
||||||
|
{:noreply, assign(socket, plans: Tasks.list_unfinished_plans())}
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, plan_changeset: changeset)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("finish-plan", %{"plan-id" => plan_id}, socket) do
|
||||||
|
{_, plan} = Tasks.finish_plan_by_id!(plan_id)
|
||||||
|
socket = put_flash(socket, :info, "finished plan \"#{plan.name}\"")
|
||||||
|
socket = assign(socket, plans: Tasks.list_unfinished_plans())
|
||||||
|
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("keydown", _params, socket) do
|
def handle_event("keydown", _params, socket) do
|
||||||
|
route = get_index_route(socket)
|
||||||
|
|
||||||
case socket.assigns.live_action do
|
case socket.assigns.live_action do
|
||||||
:index -> {:noreply, socket}
|
:index -> {:noreply, socket}
|
||||||
_ -> {:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
|
_ -> {:noreply, push_patch(socket, to: route)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,7 +174,9 @@ defmodule PlannerWeb.TasksLive do
|
||||||
socket
|
socket
|
||||||
|> refresh_tasks_and_flash_msg("task \"#{task.value}\" updated")
|
|> refresh_tasks_and_flash_msg("task \"#{task.value}\" updated")
|
||||||
|
|
||||||
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :show, task.id))}
|
route = get_index_route(socket)
|
||||||
|
|
||||||
|
{:noreply, push_patch(socket, to: route)}
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
send_update(TaskEditComponent, id: "task_edit:#{task.id}", changeset: changeset)
|
send_update(TaskEditComponent, id: "task_edit:#{task.id}", changeset: changeset)
|
||||||
|
@ -72,27 +191,83 @@ defmodule PlannerWeb.TasksLive do
|
||||||
|
|
||||||
def handle_event("delete-task", %{"task-id" => task_id}, socket) do
|
def handle_event("delete-task", %{"task-id" => task_id}, socket) do
|
||||||
{_, task} = Tasks.delete_task_by_id!(task_id)
|
{_, task} = Tasks.delete_task_by_id!(task_id)
|
||||||
{:noreply, refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" deleted")}
|
socket = refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" deleted")
|
||||||
|
route = get_index_route(socket)
|
||||||
|
|
||||||
|
{:noreply, push_patch(socket, to: route)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("new-task", %{"task" => task_params}, socket) do
|
def handle_event("new-task", %{"task" => task_params}, socket) do
|
||||||
|
add_new_task(task_params, socket.assigns.active_plan, socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("add-task-to-plan", plan_detail_params, socket) do
|
||||||
|
{_, pd} = Tasks.create_plan_detail(plan_detail_params)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
refresh_tasks_and_flash_msg(socket, "task #{pd.task_id} added to plan #{pd.plan_id}")}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("delete-task-from-plan", %{"task_id" => task_id, "plan_id" => plan_id}, socket) do
|
||||||
|
plan_detail = Tasks.get_plan_detail_by!(task_id: task_id, plan_id: plan_id)
|
||||||
|
{_, pd} = Tasks.delete_plan_detail(plan_detail)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
refresh_tasks_and_flash_msg(socket, "task #{pd.task_id} removed from plan #{pd.plan_id}")}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp refresh_tasks_and_flash_msg(socket, msg) do
|
||||||
|
tasks =
|
||||||
|
case socket.assigns.active_plan do
|
||||||
|
nil -> Tasks.list_unfinished_tasks()
|
||||||
|
plan -> Tasks.list_unfinished_tasks_by_plan_id(plan.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:tasks, tasks)
|
||||||
|
|> put_flash(:info, msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_new_task(task_params, active_plan = nil, socket) do
|
||||||
case Tasks.create_task(task_params) do
|
case Tasks.create_task(task_params) do
|
||||||
{:ok, task} ->
|
{:ok, task} ->
|
||||||
socket =
|
{:noreply, refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" created")}
|
||||||
socket
|
|
||||||
|> refresh_tasks_and_flash_msg("task \"#{task.value}\" created")
|
|
||||||
|
|
||||||
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :show, task.id))}
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
send_update(TasksComponent, id: :all_unfinished_tasks, changeset: changeset)
|
send_update(TasksComponent, id: :tasks, changeset: changeset)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp refresh_tasks_and_flash_msg(socket, msg) do
|
defp add_new_task(task_params, active_plan, socket) do
|
||||||
|
case Tasks.create_task_and_add_to_plan(task_params, active_plan) do
|
||||||
|
{:ok, %{plan_detail: _, task: task}} ->
|
||||||
|
{:noreply, refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" created")}
|
||||||
|
|
||||||
|
{:error, :task, %Ecto.Changeset{} = changeset, _} ->
|
||||||
|
send_update(TasksComponent, id: :tasks, changeset: changeset)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_plan_routes(socket, plan_id) do
|
||||||
socket
|
socket
|
||||||
|> assign(:tasks, Tasks.list_unfinished_tasks())
|
|> assign(:route_show_task, &Routes.tasks_path(&1, :show_task, plan_id, &2))
|
||||||
|> put_flash(:info, msg)
|
|> assign(:route_edit_task, &Routes.tasks_path(&1, :edit_task, plan_id, &2))
|
||||||
|
|> assign(:route_index_tasks, &Routes.tasks_path(&1, :show_plan, plan_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_task_routes(socket) do
|
||||||
|
socket
|
||||||
|
|> assign(:route_show_task, &Routes.tasks_path(&1, :show_task, &2))
|
||||||
|
|> assign(:route_edit_task, &Routes.tasks_path(&1, :edit_task, &2))
|
||||||
|
|> assign(:route_index_tasks, &Routes.tasks_path(&1, :index))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_index_route(socket) do
|
||||||
|
case socket.assigns.active_plan do
|
||||||
|
nil -> Routes.tasks_path(socket, :index)
|
||||||
|
plan -> Routes.tasks_path(socket, :show_plan, plan.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,8 +53,12 @@ defmodule PlannerWeb.Router do
|
||||||
pipe_through([:browser, :require_authenticated_user])
|
pipe_through([:browser, :require_authenticated_user])
|
||||||
|
|
||||||
live("/", TasksLive, :index)
|
live("/", TasksLive, :index)
|
||||||
live("/:task_id", TasksLive, :show)
|
live("/tasks", TasksLive, :index)
|
||||||
live("/:task_id/edit", TasksLive, :edit)
|
live("/tasks/:task_id", TasksLive, :show_task)
|
||||||
|
live("/tasks/:task_id/edit", TasksLive, :edit_task)
|
||||||
|
live("/plans/:plan_id/tasks", TasksLive, :show_plan)
|
||||||
|
live("/plans/:plan_id/tasks/:task_id", TasksLive, :show_task)
|
||||||
|
live("/plans/:plan_id/tasks/:task_id/edit", TasksLive, :edit_task)
|
||||||
|
|
||||||
get("/users/settings", UserSettingsController, :edit)
|
get("/users/settings", UserSettingsController, :edit)
|
||||||
put("/users/settings/update_password", UserSettingsController, :update_password)
|
put("/users/settings/update_password", UserSettingsController, :update_password)
|
||||||
|
|
16
priv/repo/migrations/20200809220758_create_plans.exs
Normal file
16
priv/repo/migrations/20200809220758_create_plans.exs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
defmodule Planner.Repo.Migrations.CreatePlans do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:plans, primary_key: false) do
|
||||||
|
add :id, :binary_id, primary_key: true
|
||||||
|
add :description, :string
|
||||||
|
add :finished_at, :naive_datetime
|
||||||
|
add :start, :naive_datetime
|
||||||
|
add :end, :naive_datetime
|
||||||
|
add :name, :string
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
priv/repo/migrations/20200809221238_create_plan_details.exs
Normal file
18
priv/repo/migrations/20200809221238_create_plan_details.exs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Planner.Repo.Migrations.CreatePlanDetails do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:plan_details, primary_key: false) do
|
||||||
|
add :id, :binary_id, primary_key: true
|
||||||
|
add :sort, :integer
|
||||||
|
add :task_id, references(:tasks, on_delete: :nothing, type: :binary_id)
|
||||||
|
add :plan_id, references(:plans, on_delete: :nothing, type: :binary_id)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:plan_details, [:task_id])
|
||||||
|
create index(:plan_details, [:plan_id])
|
||||||
|
create unique_index(:plan_details, [:task_id, :plan_id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,46 @@
|
||||||
# Script for populating the database. You can run it as:
|
|
||||||
#
|
|
||||||
# mix run priv/repo/seeds.exs
|
# mix run priv/repo/seeds.exs
|
||||||
#
|
|
||||||
# Inside the script, you can read and write to any of your
|
alias Planner.Tasks
|
||||||
# repositories directly:
|
|
||||||
#
|
tasks_records = [
|
||||||
# Planner.Repo.insert!(%Planner.SomeSchema{})
|
%{"value" => "task1"},
|
||||||
#
|
%{"value" => "task2"},
|
||||||
# We recommend using the bang functions (`insert!`, `update!`
|
%{"value" => "task3"},
|
||||||
# and so on) as they will fail if something goes wrong.
|
%{"value" => "task4"},
|
||||||
|
%{"value" => "task5"},
|
||||||
|
%{"value" => "task6"}
|
||||||
|
]
|
||||||
|
|
||||||
|
tasks =
|
||||||
|
Enum.map(tasks_records, fn record ->
|
||||||
|
{:ok, task} = Tasks.create_task(record)
|
||||||
|
task
|
||||||
|
end)
|
||||||
|
|
||||||
|
plans_records = [
|
||||||
|
%{name: "plan1"},
|
||||||
|
%{name: "plan2"},
|
||||||
|
%{name: "plan3"}
|
||||||
|
]
|
||||||
|
|
||||||
|
plans =
|
||||||
|
Enum.map(plans_records, fn record ->
|
||||||
|
{:ok, plan} = Tasks.create_plan(record)
|
||||||
|
plan
|
||||||
|
end)
|
||||||
|
|
||||||
|
[t1, t2, t3, t4, t5, _] = tasks
|
||||||
|
[p1, p2, _] = plans
|
||||||
|
|
||||||
|
plan_details_records = [
|
||||||
|
%{plan_id: p1.id, task_id: t1.id, sort: 0},
|
||||||
|
%{plan_id: p1.id, task_id: t2.id, sort: 0},
|
||||||
|
%{plan_id: p1.id, task_id: t3.id, sort: 0},
|
||||||
|
%{plan_id: p2.id, task_id: t4.id, sort: 0},
|
||||||
|
%{plan_id: p2.id, task_id: t5.id, sort: 0}
|
||||||
|
# deliberately leave off the last task
|
||||||
|
]
|
||||||
|
|
||||||
|
Enum.each(plan_details_records, fn record ->
|
||||||
|
Tasks.create_plan_detail(record)
|
||||||
|
end)
|
||||||
|
|
Loading…
Add table
Reference in a new issue