Skip to content

Commit

Permalink
Filter items by status
Browse files Browse the repository at this point in the history
- Display footer with All, Active, Done and Archived links
- Filter items by status using LiveView

ref: #119
  • Loading branch information
SimonLab committed Aug 10, 2022
1 parent 2e9dcdd commit e33c930
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 50 deletions.
71 changes: 43 additions & 28 deletions lib/app/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ defmodule App.Item do
|> Repo.update()
end


# 🐲 H E R E B E D R A G O N S! 🐉
# ⏳ Working with Time is all Dragons! 🙄
# 👩‍💻 Feedback/Pairing/Refactoring Welcome! 🙏
Expand Down Expand Up @@ -124,6 +123,18 @@ defmodule App.Item do
|> accumulate_item_timers()
end

def all_items_with_timers(person_id \\ 0) do
sql = """
SELECT i.id, i.text, i.status, i.person_id, t.start, t.stop, t.id as timer_id FROM items i
FULL JOIN timers as t ON t.item_id = i.id
WHERE i.person_id = $1 AND i.status IS NOT NULL
ORDER BY timer_id ASC;
"""

Ecto.Adapters.SQL.query!(Repo, sql, [person_id])
|> map_columns_to_values()
|> accumulate_item_timers()
end

@doc """
`map_columns_to_values/1` takes an Ecto SQL query result
Expand All @@ -134,9 +145,8 @@ defmodule App.Item do
ref: https://groups.google.com/g/elixir-ecto/c/0cubhSd3QS0/m/DLdQsFrcBAAJ
"""
def map_columns_to_values(res) do
Enum.map(res.rows, fn(row) ->
Enum.zip(res.columns, row)
|> Map.new |> AtomicMap.convert()
Enum.map(res.rows, fn row ->
Enum.zip(res.columns, row) |> Map.new() |> AtomicMap.convert()
end)
end

Expand All @@ -163,9 +173,9 @@ defmodule App.Item do
Map.new(list, fn item ->
if is_nil(item.timer_id) do
# item without any active timer
{ 0, 0}
{0, 0}
else
{ item.timer_id, timer_diff(item)}
{item.timer_id, timer_diff(item)}
end
end)
end
Expand Down Expand Up @@ -208,38 +218,43 @@ defmodule App.Item do
timer_id_diff_map = map_timer_diff(items_with_timers)

# e.g: %{1 => [2, 1], 2 => [4, 3], 3 => []}
item_id_timer_id_map = Map.new(items_with_timers, fn i ->
{ i.id, Enum.map(items_with_timers, fn it ->
if i.id == it.id, do: it.timer_id, else: nil
end)
# stackoverflow.com/questions/46339815/remove-nil-from-list
|> Enum.reject(&is_nil/1)
}
end)
item_id_timer_id_map =
Map.new(items_with_timers, fn i ->
{i.id,
Enum.map(items_with_timers, fn it ->
if i.id == it.id, do: it.timer_id, else: nil
end)
# stackoverflow.com/questions/46339815/remove-nil-from-list
|> Enum.reject(&is_nil/1)}
end)

# this one is "wasteful" but I can't think of how to simplify it ...
item_id_timer_diff_map = Map.new(items_with_timers, fn item ->
timer_id_list = Map.get(item_id_timer_id_map, item.id, [0])
# Remove last item from list before summing to avoid double-counting
{_, timer_id_list} = List.pop_at(timer_id_list, -1)

{ item.id, Enum.reduce(timer_id_list, 0, fn timer_id, acc ->
Map.get(timer_id_diff_map, timer_id) + acc
end)
}
end)
item_id_timer_diff_map =
Map.new(items_with_timers, fn item ->
timer_id_list = Map.get(item_id_timer_id_map, item.id, [0])
# Remove last item from list before summing to avoid double-counting
{_, timer_id_list} = List.pop_at(timer_id_list, -1)

{item.id,
Enum.reduce(timer_id_list, 0, fn timer_id, acc ->
Map.get(timer_id_diff_map, timer_id) + acc
end)}
end)

# creates a nested map: %{ item.id: %{id: 1, text: "my item", etc.}}
Map.new(items_with_timers, fn item ->
time_elapsed = Map.get(item_id_timer_diff_map, item.id)
start = if is_nil(item.start), do: nil,
else: NaiveDateTime.add(item.start, -time_elapsed)

{ item.id, %{item | start: start}}
start =
if is_nil(item.start),
do: nil,
else: NaiveDateTime.add(item.start, -time_elapsed)

{item.id, %{item | start: start}}
end)
# Return the list of items without duplicates and only the last/active timer:
|> Map.values()
# Sort list by item.id descending (ordered by timer_id ASC above) so newest item first:
|> Enum.sort_by(fn(i) -> i.id end, :desc)
|> Enum.sort_by(fn i -> i.id end, :desc)
end
end
80 changes: 58 additions & 22 deletions lib/app_web/live/app_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ defmodule AppWeb.AppLive do
# assign default values to socket:
defp assign_socket(socket) do
person_id = get_person_id(socket.assigns)
assign(socket, items: Item.items_with_timers(person_id), active: %Item{}, editing: nil)

assign(socket,
items: Item.items_with_timers(person_id),
active: %Item{},
editing: nil
)
end

@impl true
Expand Down Expand Up @@ -64,6 +69,7 @@ defmodule AppWeb.AppLive do
def handle_event("start", data, socket) do
item = Item.get_item!(Map.get(data, "id"))
person_id = get_person_id(socket.assigns)

{:ok, _timer} =
Timer.start(%{
item_id: item.id,
Expand Down Expand Up @@ -121,6 +127,8 @@ defmodule AppWeb.AppLive do

# Check for status 4 (:done)
def done?(item), do: item.status == 4
# Check for status 4 (:done)
def archived?(item), do: item.status == 6

# Check if an item has an active timer
def started?(item) do
Expand All @@ -142,7 +150,6 @@ defmodule AppWeb.AppLive do
|> DateTime.to_unix(:millisecond)
end


# Elixir implementation of `timer_text/2`
def leftPad(val) do
if val < 10, do: "0#{to_string(val)}", else: val
Expand All @@ -155,32 +162,61 @@ defmodule AppWeb.AppLive do
diff = timestamp(item.stop) - timestamp(item.start)

# seconds
s = if diff > 1000 do
s = diff / 1000 |> trunc()
s = if s > 60, do: Integer.mod(s, 60), else: s
leftPad(s)
else
"00"
end
s =
if diff > 1000 do
s = (diff / 1000) |> trunc()
s = if s > 60, do: Integer.mod(s, 60), else: s
leftPad(s)
else
"00"
end

# minutes
m = if diff > 60000 do
m = diff / 60000 |> trunc()
m = if m > 60, do: Integer.mod(m, 60), else: m
leftPad(m)
else
"00"
end
m =
if diff > 60000 do
m = (diff / 60000) |> trunc()
m = if m > 60, do: Integer.mod(m, 60), else: m
leftPad(m)
else
"00"
end

# hours
h = if diff > 3600000 do
h = diff / 3600000 |> trunc()
leftPad(h)
else
"00"
end
h =
if diff > 3_600_000 do
h = (diff / 3_600_000) |> trunc()
leftPad(h)
else
"00"
end

"#{h}:#{m}:#{s}"
end
end

# Filter element by status (all, active, archived)
# see https://hexdocs.pm/phoenix_live_view/live-navigation.html
@impl true
def handle_params(params, _uri, socket) do
IO.inspect(params)
person_id = get_person_id(socket.assigns)
items = Item.all_items_with_timers(person_id)

case params["filter_by"] do
"active" ->
items = Enum.filter(items, &(&1.status == 2))
{:noreply, assign(socket, items: items)}

"done" ->
items = Enum.filter(items, &(&1.status == 4))
{:noreply, assign(socket, items: items)}

"archived" ->
items = Enum.filter(items, &(&1.status == 6))
{:noreply, assign(socket, items: items)}

_ ->
{:noreply, assign(socket, items: items)}
end
end
end
38 changes: 38 additions & 0 deletions lib/app_web/live/app_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@
<%= for item <- @items do %>
<li data-id={item.id} class="mt-2 flex w-full border-t border-slate-200 py-2">


<%= if archived?(item) do %>
<input type="checkbox" phx-value-id={item.id} phx-click="toggle"
class="flex-none p-4 m-2 form-checkbox text-slate-400 cursor-not-allowed"
checked disabled />
<label class="w-full text-slate-400 m-2 line-through">
<%= item.text %>
</label>

<div class="flex flex-col">
<div class="flex flex-col justify-end mr-1">
<button disabled class="cursor-not-allowed inline-flex items-center px-2 py-1 mr-2 h-9
bg-gray-200 text-gray-800 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
Archived
</button>
</div>
</div>


<% else %>
<!-- if item is "done" (status: 4) strike-through and show "Archive" button -->
<%= if done?(item) do %>
<input type="checkbox" phx-value-id={item.id} phx-click="toggle"
Expand Down Expand Up @@ -207,12 +233,24 @@
</button>
<% end %><!-- end timers_any?(item) -->
<% end %>
<% end %>

</li>
<% end %><!-- end for item <- @items -->
</ul>
</div>

<footer>
<div class="flex flex-row">
<div class="px-8"><%= live_patch "All", to: Routes.live_path(@socket, AppWeb.AppLive, %{filter_by: "all"} ) %></div>
<div class="px-8"><%= live_patch "Active", to: Routes.live_path(@socket, AppWeb.AppLive, %{filter_by: "active"} ) %></div>
<div class="px-8"><%= live_patch "Done", to: Routes.live_path(@socket, AppWeb.AppLive, %{filter_by: "done"} ) %></div>
<div class="px-8"><%= live_patch "Archived", to: Routes.live_path(@socket, AppWeb.AppLive, %{filter_by: "archived"} ) %></div>
</div>
</footer>



<script>
function leftPad(val) {
return val < 10 ? '0' + String(val) : val;
Expand Down

0 comments on commit e33c930

Please sign in to comment.