Skip to content

Commit 041ab31

Browse files
authored
feat: tables page (#51)
1 parent b3b647d commit 041ab31

6 files changed

Lines changed: 203 additions & 15 deletions

File tree

lib/gallium/ticketing.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ defmodule Gallium.Ticketing do
2020
|> Repo.preload([:payment, :accompany, user: :ticket])
2121
end
2222

23+
@doc """
24+
Groups attendees (and their accompanies) by `table_preference`.
25+
"""
26+
def list_tables_with_attendees do
27+
Attendee
28+
|> where([a], not is_nil(a.table_preference) and a.table_preference != "")
29+
|> order_by([a], asc: a.table_preference, asc: a.inserted_at)
30+
|> Repo.all()
31+
|> Repo.preload(:accompany)
32+
|> Enum.group_by(& &1.table_preference)
33+
|> Enum.map(fn {table, attendees} -> {table, table_people(attendees)} end)
34+
|> Enum.sort_by(fn {table, _} -> table end)
35+
end
36+
37+
defp table_people(attendees) do
38+
Enum.flat_map(attendees, &attendee_and_accompany/1)
39+
end
40+
41+
defp attendee_and_accompany(%Attendee{accompany: nil} = attendee) do
42+
[%{full_name: attendee.full_name}]
43+
end
44+
45+
defp attendee_and_accompany(%Attendee{accompany: accompany} = attendee) do
46+
[%{full_name: attendee.full_name}, %{full_name: accompany.full_name}]
47+
end
48+
2349
@doc """
2450
Returns the list of accompanies.
2551

lib/gallium_web/live/backoffice/components/sidebar.ex

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ defmodule GalliumWeb.BackOffice.Components.Sidebar do
77

88
def sidebar(assigns) do
99
menu_items = [
10-
%{
11-
action: :members,
12-
icon: "hero-arrow-up-tray",
13-
label: "Adicionar Membros",
14-
path: ~p"/dashboard/members"
15-
},
1610
%{
1711
action: :attendees,
1812
icon: "hero-users",
1913
label: "Inscrições",
2014
path: ~p"/dashboard/attendees"
15+
},
16+
%{
17+
action: :tables,
18+
icon: "hero-rectangle-group",
19+
label: "Mesas",
20+
path: ~p"/dashboard/tables"
21+
},
22+
%{
23+
action: :members,
24+
icon: "hero-arrow-up-tray",
25+
label: "Sócios",
26+
path: ~p"/dashboard/members"
2127
}
2228
]
2329

@@ -28,8 +34,7 @@ defmodule GalliumWeb.BackOffice.Components.Sidebar do
2834
"fixed left-0 top-0 h-screen w-64 flex-shrink-0 md:relative md:h-auto md:border-r-2 md:border-gray-300 z-50 transition-transform duration-300 overflow-y-auto bg-white md:bg-transparent",
2935
if(@sidebar_open, do: "translate-x-0", else: "-translate-x-full md:translate-x-0")
3036
]}>
31-
<div class="md:hidden px-4 py-3 border-b border-gray-200 flex items-center justify-between">
32-
<h2 class="font-bold font-amarante text-black">Menu</h2>
37+
<div class="md:hidden px-4 py-3 border-b border-gray-200 flex items-center justify-end">
3338
<button
3439
phx-click="toggle_sidebar"
3540
class="hover:bg-gray-300 px-2 py-1 rounded-md"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
defmodule GalliumWeb.BackOffice.TablesLive.Index do
2+
@moduledoc """
3+
LiveView showing seating arrangement: one rectangle per table with avatars
4+
for each person around it.
5+
"""
6+
use GalliumWeb, :live_view
7+
8+
import GalliumWeb.BackOffice.Components.BackofficeLayout
9+
10+
alias Gallium.Ticketing
11+
12+
attr :name, :string, required: true
13+
attr :people, :list, required: true
14+
15+
def table_card(assigns) do
16+
{top, bottom} = split_people(assigns.people)
17+
assigns = assigns |> assign(:top, top) |> assign(:bottom, bottom)
18+
19+
~H"""
20+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm px-6 py-8">
21+
<div class="flex flex-col items-center gap-3">
22+
<div class="flex justify-center gap-3 min-h-10">
23+
<.avatar :for={person <- @top} person={person} />
24+
</div>
25+
26+
<div class="w-full max-w-md h-28 rounded-lg bg-olive text-white flex items-center justify-center shadow-inner">
27+
<span class="font-amarante text-2xl uppercase tracking-widest">{@name}</span>
28+
</div>
29+
30+
<div class="flex justify-center gap-3 min-h-10">
31+
<.avatar :for={person <- @bottom} person={person} />
32+
</div>
33+
</div>
34+
</div>
35+
"""
36+
end
37+
38+
attr :person, :map, required: true
39+
40+
defp avatar(assigns) do
41+
~H"""
42+
<div class="relative group">
43+
<div
44+
class="w-10 h-10 rounded-full bg-olive-100 text-olive-700 border border-olive-200 flex items-center justify-center font-amarante text-sm font-bold cursor-default select-none"
45+
title={@person.full_name}
46+
>
47+
{@person.initials}
48+
</div>
49+
<span class="pointer-events-none absolute left-1/2 -translate-x-1/2 -top-9 whitespace-nowrap rounded bg-gray-900 text-white text-xs px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity font-cormorant z-10">
50+
{@person.full_name}
51+
</span>
52+
</div>
53+
"""
54+
end
55+
56+
defp split_people(people) do
57+
count = length(people)
58+
half = div(count + 1, 2)
59+
Enum.split(people, half)
60+
end
61+
62+
def mount(_params, _session, socket) do
63+
tables =
64+
Ticketing.list_tables_with_attendees()
65+
|> Enum.map(fn {name, people} ->
66+
%{
67+
name: name,
68+
people: Enum.map(people, fn p -> Map.put(p, :initials, initials(p.full_name)) end)
69+
}
70+
end)
71+
72+
socket =
73+
socket
74+
|> assign(:current_page, :tables)
75+
|> assign(:sidebar_open, false)
76+
|> assign(:tables, tables)
77+
78+
{:ok, socket}
79+
end
80+
81+
def handle_event("toggle_sidebar", _params, socket) do
82+
{:noreply, assign(socket, :sidebar_open, !socket.assigns.sidebar_open)}
83+
end
84+
85+
defp initials(nil), do: "?"
86+
87+
defp initials(full_name) do
88+
parts =
89+
full_name
90+
|> String.split(~r/\s+/, trim: true)
91+
|> Enum.reject(&(&1 == ""))
92+
93+
case parts do
94+
[] ->
95+
"?"
96+
97+
[one] ->
98+
String.upcase(String.slice(one, 0, 1))
99+
100+
list ->
101+
String.upcase(String.slice(List.first(list), 0, 1) <> String.slice(List.last(list), 0, 1))
102+
end
103+
end
104+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<.backoffice_layout sidebar_open={@sidebar_open} current_page={@current_page} flash={@flash}>
2+
<div class="w-full">
3+
<div class="flex flex-col gap-6">
4+
<div class="mb-2">
5+
<h1 class="text-4xl font-bold text-gray-content font-amarante">Mesas</h1>
6+
<p class="text-gray mt-2 font-cormorant">
7+
Distribuição de convidados por mesa.
8+
</p>
9+
</div>
10+
11+
<%= if @tables == [] do %>
12+
<div class="flex flex-col items-center justify-center py-24 text-gray-300 font-cormorant bg-white rounded-xl border border-gray-200 shadow-sm">
13+
<.icon name="hero-rectangle-group" class="size-14 mb-4" />
14+
<p class="text-lg">Ainda não há mesas atribuídas.</p>
15+
</div>
16+
<% else %>
17+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
18+
<%= for table <- @tables do %>
19+
<.table_card name={table.name} people={table.people} />
20+
<% end %>
21+
</div>
22+
<% end %>
23+
</div>
24+
</div>
25+
</.backoffice_layout>

lib/gallium_web/router.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ defmodule GalliumWeb.Router do
6060
{GalliumWeb.UserAuth, :require_authenticated},
6161
{GalliumWeb.UserAuth, :require_admin}
6262
] do
63-
live "/", BackOffice.MembersLive.Index
63+
live "/", BackOffice.AttendeesLive.Index
6464
live "/members", BackOffice.MembersLive.Index
6565
live "/attendees", BackOffice.AttendeesLive.Index
66+
live "/tables", BackOffice.TablesLive.Index
6667
end
6768
end
6869

priv/repo/seeds/ticketing.exs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ defmodule Gallium.Repo.Seeds.Ticketing do
3535
10 => "a100010"
3636
}
3737

38+
@full_names %{
39+
1 => "Ana Maria Silva",
40+
2 => "Bruno Costa Pereira",
41+
3 => "Catarina Dias Lobo",
42+
4 => "Diogo Ferreira",
43+
5 => "Eva Marques Sousa",
44+
6 => "Filipe Almeida",
45+
7 => "Gabriela Nunes",
46+
8 => "Hugo Ramos Antunes",
47+
9 => "Inês Carvalho",
48+
10 => "João Lobo"
49+
}
50+
51+
@table_preferences %{
52+
1 => "Mesa 1",
53+
2 => "Mesa 1",
54+
3 => "Mesa 1",
55+
4 => "Mesa 2",
56+
5 => "Mesa 2",
57+
6 => "Mesa 2",
58+
7 => "Mesa 3",
59+
8 => "Mesa 3",
60+
9 => "Mesa 3",
61+
10 => "Mesa 3"
62+
}
63+
3864
@accompany_data %{
3965
1 => %{full_name: "Acompanhante de Ana", email: "acomp1@example.com", phone_number: "+351910000001"},
4066
2 => %{full_name: "Acompanhante de Bruno", email: "acomp2@example.com", phone_number: "+351910000002"},
@@ -78,12 +104,13 @@ defmodule Gallium.Repo.Seeds.Ticketing do
78104
is_member = index <= 5
79105

80106
attrs = %{
81-
full_name: "Attendee #{index}",
82-
phone_number: "+35191000000#{index}",
83-
is_cesium_member: is_member,
84-
user_id: user_id,
85-
student_number: @student_numbers[index],
86-
nif: "20000000#{index}"
107+
full_name: @full_names[index] || "Attendee #{index}",
108+
phone_number: "+35191000000#{index}",
109+
is_cesium_member: is_member,
110+
user_id: user_id,
111+
student_number: @student_numbers[index],
112+
nif: "20000000#{index}",
113+
table_preference: @table_preferences[index]
87114
}
88115

89116
case Ticketing.create_attendee(attrs) do

0 commit comments

Comments
 (0)