action-requests-demo.jamey..../lib/action_requests_demo_web/live/action_requests_live.ex
James Greenwood ce67d84865
All checks were successful
build / build (push) Successful in 27s
drop debounce to 100ms
2025-11-17 14:37:55 +00:00

302 lines
12 KiB
Elixir

defmodule ActionRequestsDemoWeb.ActionRequestsLive do
use ActionRequestsDemoWeb, :live_view
alias ActionRequestsDemo.ActionRequests
def mount(_params, _session, socket) do
{:ok, assign(socket, current_user_id: 1)}
end
def handle_params(params, _uri, socket) do
case ActionRequests.list_action_requests(params, socket.assigns.current_user_id) do
{:ok, {action_requests, meta}} ->
{:noreply,
socket
|> assign(:action_requests, action_requests)
|> assign(:meta, meta)
|> assign(:params, params)}
{:error, _meta} ->
{:noreply,
socket
|> assign(:action_requests, [])
|> assign(:meta, %Flop.Meta{})
|> assign(:params, params)}
end
end
def handle_event("filter", params, socket) do
# Navigate with new params, which will trigger handle_params
{:noreply, push_patch(socket, to: ~p"/?#{params}")}
end
def handle_event("patch", params, socket) do
# Handle pagination events from paginator links
{:noreply, push_patch(socket, to: ~p"/?#{params}")}
end
def render(assigns) do
~H"""
<div class="px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto max-w-7xl">
<h1 class="text-3xl font-bold mb-8">Action Requests</h1>
<.form
for={%{}}
method="get"
action={~p"/"}
phx-submit="filter"
phx-change="filter"
class="mb-6"
>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label class="block text-sm font-medium text-white mb-1">Status</label>
<select
name="status"
class="w-full rounded-md border border-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2"
>
<option value="" selected={@params["status"] in [nil, "", "all"]}>
All
</option>
<option value="resolved" selected={@params["status"] == "resolved"}>
Resolved
</option>
<option value="unresolved" selected={@params["status"] == "unresolved"}>
Unresolved
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Assignment</label>
<select
name="assignment"
class="w-full rounded-md border border-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2"
>
<option value="" selected={@params["assignment"] in [nil, ""]}>
All
</option>
<option value="mine" selected={@params["assignment"] == "mine"}>
Mine
</option>
<option value="assigned" selected={@params["assignment"] == "assigned"}>
Assigned
</option>
<option value="unassigned" selected={@params["assignment"] == "unassigned"}>
Unassigned
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Patient Name</label>
<input
type="text"
name="patient_name"
value={@params["patient_name"] || ""}
placeholder="Search by name..."
autocomplete="off"
class="w-full rounded-md border border-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 leading-4"
phx-debounce="100"
/>
</div>
<div class="flex items-end">
<noscript>
<button
type="submit"
class="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500"
>
Apply Filters
</button>
</noscript>
</div>
</div>
</.form>
<div class="overflow-x-auto rounded-lg border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
<.sort_link meta={@meta} params={@params} field={:patient_name}>Patient Name</.sort_link>
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
<.sort_link meta={@meta} params={@params} field={:status}>Status</.sort_link>
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Assigned To
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
<.sort_link meta={@meta} params={@params} field={:inserted_at}>Created</.sort_link>
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
<.sort_link meta={@meta} params={@params} field={:delivery_scheduled_at}>
Delivery Scheduled
</.sort_link>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<%= for request <- @action_requests do %>
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{request.patient_name}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
<.status_badge status={request.status} />
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{if request.assigned_user_id, do: "User ##{request.assigned_user_id}", else: "-"}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{Calendar.strftime(request.inserted_at, "%Y-%m-%d %H:%M")}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<%= if request.delivery_scheduled_at do %>
{Calendar.strftime(request.delivery_scheduled_at, "%Y-%m-%d %H:%M")}
<% else %>
-
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between">
<div class="text-sm text-white">
Page {@meta.current_page} of {@meta.total_pages} (showing {length(@action_requests)} of {@meta.total_count} records)
</div>
<nav aria-label="Pagination" class="flex flex-wrap gap-1 items-center justify-center">
<.link
patch={~p"/?#{Map.merge(@params, %{"page" => 1})}"}
class={[
"rounded-md px-2 py-1 text-sm font-semibold",
(@meta.current_page == 1 && "bg-blue-600 text-white") ||
"bg-white text-gray-900 hover:bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300"
]}
aria-current={@meta.current_page == 1 && "page"}
>
1
</.link>
<%= if @meta.current_page > 4 do %>
<span class="px-2 py-1 text-gray-500">...</span>
<% end %>
<%= for page <- page_window(@meta.current_page, @meta.total_pages, 2) do %>
<%= if page != 1 and page != @meta.total_pages do %>
<.link
patch={~p"/?#{Map.merge(@params, %{"page" => page})}"}
class={[
"rounded-md px-2 py-1 text-sm font-semibold",
(page == @meta.current_page && "bg-blue-600 text-white") ||
"bg-white text-gray-900 hover:bg-blue-50 shadow-sm ring-1 ring-inset ring-gray-300"
]}
aria-current={page == @meta.current_page && "page"}
>
{page}
</.link>
<% end %>
<% end %>
<%= if @meta.current_page < @meta.total_pages - 3 do %>
<span class="px-2 py-1 text-gray-500">...</span>
<% end %>
<%= if @meta.total_pages > 1 do %>
<.link
patch={~p"/?#{Map.merge(@params, %{"page" => @meta.total_pages})}"}
class={[
"rounded-md px-2 py-1 text-sm font-semibold",
(@meta.current_page == @meta.total_pages && "bg-blue-600 text-white") ||
"bg-white text-gray-900 hover:bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300"
]}
aria-current={@meta.current_page == @meta.total_pages && "page"}
>
{@meta.total_pages}
</.link>
<% end %>
<.link
patch={~p"/?#{Map.merge(@params, %{"page" => @meta.previous_page})}"}
class={[
"rounded-md px-2 py-1 text-sm font-semibold",
(!@meta.has_previous_page? && "bg-gray-200 text-gray-500 cursor-not-allowed") ||
"bg-white text-gray-900 hover:bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300"
]}
aria-disabled={!@meta.has_previous_page?}
>
Previous
</.link>
<.link
patch={~p"/?#{Map.merge(@params, %{"page" => @meta.next_page})}"}
class={[
"rounded-md px-2 py-1 text-sm font-semibold",
(!@meta.has_next_page? && "bg-gray-200 text-gray-500 cursor-not-allowed") ||
"bg-white text-gray-900 hover:bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300"
]}
aria-disabled={!@meta.has_next_page?}
>
Next
</.link>
</nav>
</div>
</div>
</div>
"""
end
defp page_window(current, total, radius) do
# Returns a list of page numbers centered around current page, excluding first/last
start_page = max(current - radius, 2)
end_page = min(current + radius, total - 1)
if end_page >= start_page do
Enum.to_list(start_page..end_page)
else
[]
end
end
defp status_badge(assigns) do
~H"""
<span class={[
"inline-flex rounded-full px-2 py-1 text-xs font-semibold leading-5",
if(@status == "resolved",
do: "bg-green-100 text-green-800",
else: "bg-yellow-100 text-yellow-800"
)
]}>
{@status}
</span>
"""
end
defp sort_link(assigns) do
current_order =
if assigns.meta.flop.order_by == [assigns.field],
do: List.first(assigns.meta.flop.order_directions),
else: nil
next_direction = if current_order == :asc, do: :desc, else: :asc
assigns = assign(assigns, :next_direction, next_direction)
assigns = assign(assigns, :current_order, current_order)
~H"""
<.link
patch={
~p"/?#{Map.merge(@params, %{"order_by" => @field, "order_directions" => @next_direction})}"
}
class="group inline-flex"
>
{render_slot(@inner_block)}
<%= if @current_order == :asc do %>
<span class="ml-2">↑</span>
<% end %>
<%= if @current_order == :desc do %>
<span class="ml-2">↓</span>
<% end %>
</.link>
"""
end
end