oneshot
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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
|
||||
Reference in New Issue
Block a user