Files
rsh26_pool_example/lib/rsh26pool_web/live/manage_live.ex
T

238 lines
9.0 KiB
Elixir
Raw Normal View History

2026-07-04 19:27:17 +09:00
defmodule Rsh26poolWeb.ManageLive do
@moduledoc """
The owner's management page: rename the room, end voting, and watch results
(with vote counts) and participants update in real time. In grouping mode the
computed teams are shown once voting has ended.
"""
use Rsh26poolWeb, :live_view
alias Rsh26pool.JST
alias Rsh26pool.Voting
@impl true
def mount(%{"room_number" => number}, _session, socket) do
room = number |> Voting.get_room_by_number() |> Voting.ensure_closed()
if connected?(socket), do: Voting.subscribe(room.id)
{:ok,
socket
|> assign(:room, room)
|> assign(:participant, socket.assigns.current_participant)
|> assign(:page_title, "管理 - #{room.name}")
|> assign(:name_form, to_form(Voting.change_room_settings(room)))
|> load_state()}
end
@impl true
def handle_event("rename", %{"room" => params}, socket) do
case Voting.update_room_settings(socket.assigns.room, params) do
{:ok, room} ->
{:noreply,
socket
|> assign(:room, room)
|> assign(:name_form, to_form(Voting.change_room_settings(room)))
|> put_flash(:info, "ルーム名を変更しました。")}
{:error, changeset} ->
{:noreply, assign(socket, :name_form, to_form(changeset))}
end
end
def handle_event("close", _params, socket) do
{:ok, room} = Voting.close_room(socket.assigns.room)
{:noreply,
socket
|> assign(:room, room)
|> put_flash(:info, "投票を終了しました。")
|> load_state()}
end
@impl true
def handle_info({:voting, :room_updated}, socket) do
room =
socket.assigns.room.room_number |> Voting.get_room_by_number() |> Voting.ensure_closed()
{:noreply, socket |> assign(:room, room) |> load_state()}
end
def handle_info({:voting, _event}, socket) do
{:noreply, load_state(socket)}
end
## State ---------------------------------------------------------------
defp load_state(socket) do
room = socket.assigns.room
open? = Voting.room_open?(room)
socket
|> assign(:open?, open?)
|> assign(:results, Voting.results(room))
|> assign(:participants, Voting.list_participants(room))
|> assign(:grouping, if(not open? and room.grouping_mode, do: Voting.group_assignments(room)))
end
## Render --------------------------------------------------------------
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_participant={@current_participant}>
<.header>
管理ページ
<:subtitle>{@room.name}</:subtitle>
<:actions>
<span :if={@open?} class="badge badge-success gap-1">
<.icon name="hero-hand-raised" class="size-4" /> 投票受付中
</span>
<span :if={not @open?} class="badge badge-neutral gap-1">
<.icon name="hero-lock-closed" class="size-4" /> 終了
</span>
</:actions>
</.header>
<div class="card bg-base-100 border border-base-200 shadow-sm">
<div class="card-body py-4">
<p class="text-sm text-base-content/70">参加者に伝えるルーム番号</p>
<p class="text-3xl font-mono font-bold tracking-widest">{@room.room_number}</p>
<p class="text-sm text-base-content/60">
投票ページ:<span class="font-mono">{url(~p"/vote/#{@room.room_number}")}</span>
</p>
<p :if={@room.closes_at} class="text-sm text-base-content/70 flex items-center gap-1 mt-1">
<.icon name="hero-clock" class="size-4" /> 自動終了予定:{JST.format(@room.closes_at)}(日本時間)
</p>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="card bg-base-100 border border-base-200 shadow-sm">
<div class="card-body py-4">
<h2 class="card-title text-base">
<.icon name="hero-pencil-square" class="size-5" /> ルーム名を変更
</h2>
<.form for={@name_form} phx-submit="rename" id="rename-form" class="flex flex-col gap-2">
<.input field={@name_form[:name]} type="text" />
<div class="flex justify-end">
<button type="submit" class="btn btn-primary btn-sm">保存</button>
</div>
</.form>
</div>
</div>
<div class="card bg-base-100 border border-base-200 shadow-sm">
<div class="card-body py-4">
<h2 class="card-title text-base">
<.icon name="hero-lock-closed" class="size-5" /> 投票の終了
</h2>
<%= if @open? do %>
<p class="text-sm text-base-content/70">
投票を終了すると、参加者は投票できなくなります。 <span :if={@room.grouping_mode}>終了時にグループ分けが行われます。</span>
</p>
<div class="flex justify-end">
<button
type="button"
phx-click="close"
data-confirm="投票を終了しますか?この操作は取り消せません。"
class="btn btn-error btn-sm"
>
投票を終了する
</button>
</div>
<% else %>
<p class="text-sm text-base-content/70">この投票は終了しています。</p>
<% end %>
</div>
</div>
</div>
<div>
<h2 class="font-semibold mb-2 flex items-center gap-2">
<.icon name="hero-trophy" class="size-5 text-warning" /> 現在の結果
<span class="badge badge-sm">{length(@results)} 件</span>
</h2>
<p :if={@results == []} class="text-base-content/60 text-sm">まだ投稿がありません。</p>
<ol class="space-y-2">
<li
:for={{result, index} <- Enum.with_index(@results)}
class={[
"flex items-center justify-between gap-3 p-3 rounded-box border",
index == 0 && @results != [] && "border-warning bg-warning/10",
index != 0 && "border-base-200 bg-base-100"
]}
>
<div class="flex items-center gap-3 min-w-0">
<span class="font-bold text-lg w-6 text-center">{index + 1}</span>
<div class="min-w-0">
<p class="truncate">{result.idea.text}</p>
<p :if={@room.grouping_mode} class="text-xs text-base-content/60">
提案者:{result.idea.participant.name}
</p>
</div>
</div>
<span class="badge badge-lg gap-1">
<.icon name="hero-hand-thumb-up" class="size-4" /> {result.count}
</span>
</li>
</ol>
</div>
<div :if={@grouping}>
<h2 class="font-semibold mb-2 flex items-center gap-2">
<.icon name="hero-user-group" class="size-5 text-primary" /> グループ分けの結果
</h2>
<p :if={@grouping.groups == []} class="text-base-content/70 text-sm">
リーダーになる人が決まりませんでした(十分な票を集めた意見がありません)。
</p>
<div class="grid gap-3 sm:grid-cols-2">
<div
:for={group <- @grouping.groups}
class="card bg-base-100 border border-base-200 shadow-sm"
>
<div class="card-body py-4">
<h3 class="font-bold flex items-center gap-2">
<.icon name="hero-user-group" class="size-5 text-primary" />
{group.leader.name} グループ
</h3>
<p class="text-sm text-base-content/70">テーマ:{group.opinion && group.opinion.text}</p>
<ul class="mt-2 space-y-1 text-sm">
<li class="flex items-center gap-2 font-semibold">
<.icon name="hero-trophy" class="size-4 text-warning" /> {group.leader.name}(リーダー)
</li>
<li :for={member <- group.members} class="flex items-center gap-2 pl-6">
{member.name}
</li>
</ul>
</div>
</div>
</div>
<p :if={@grouping.ungrouped != []} class="text-sm text-base-content/60 mt-2">
未割り当て:{Enum.map_join(@grouping.ungrouped, "、", & &1.name)}
</p>
</div>
<div>
<h2 class="font-semibold mb-2 flex items-center gap-2">
<.icon name="hero-users" class="size-5" /> 参加者
<span class="badge badge-sm">{length(@participants)} 人</span>
</h2>
<div class="flex flex-wrap gap-2">
<span
:for={p <- @participants}
class={[
"badge badge-lg",
p.role == "owner" && "badge-primary",
p.role != "owner" && "badge-ghost"
]}
>
{p.name}
<span :if={p.role == "owner"} class="ml-1 text-xs">(オーナー)</span>
</span>
</div>
</div>
</Layouts.app>
"""
end
end