Skip to content

Commit 2d0ae3c

Browse files
feat: badge revokes (#526)
Co-authored-by: Nuno Miguel <nmpf.2005@gmail.com>
1 parent 90ab1bf commit 2d0ae3c

File tree

12 files changed

+322
-7
lines changed

12 files changed

+322
-7
lines changed

lib/safira/contest.ex

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ defmodule Safira.Contest do
104104
|> Repo.all()
105105
end
106106

107+
def list_attendee_redeems_meta(attendee_id, params) do
108+
BadgeRedeem
109+
|> join(:inner, [br], b in Badge, on: b.id == br.badge_id, as: :badge)
110+
|> where([br, b], br.attendee_id == ^attendee_id)
111+
|> preload([:badge, attendee: [:user], redeemed_by: [:user]])
112+
|> order_by([br], desc: br.inserted_at)
113+
|> Flop.validate_and_run(params, for: BadgeRedeem)
114+
end
115+
107116
@doc """
108117
Lists all badge redeems belonging to a badge.
109118
@@ -168,7 +177,7 @@ defmodule Safira.Contest do
168177
|> where([br], br.badge_id == ^badge_id)
169178
|> join(:inner, [br], a in assoc(br, :attendee), as: :attendee)
170179
|> join(:inner, [br, a], u in assoc(a, :user), as: :user)
171-
|> preload(attendee: [:user])
180+
|> preload(attendee: [:user], redeemed_by: [:user])
172181
|> apply_filters(opts)
173182
|> Flop.validate_and_run(params, for: BadgeRedeem)
174183
end
@@ -376,6 +385,32 @@ defmodule Safira.Contest do
376385
Repo.get!(DailyPrize, id)
377386
end
378387

388+
def revoke_badge_redeem_from_attendee(badge_redeem_id) do
389+
revoke_badge_redeem_transaction(badge_redeem_id)
390+
end
391+
392+
defp revoke_badge_redeem_transaction(badge_redeem_id) do
393+
Multi.new()
394+
|> Multi.run(:badge_redeem, fn _repo, _changes ->
395+
{:ok, get_badge_redeem!(badge_redeem_id, preloads: [:badge, :attendee])}
396+
end)
397+
|> Multi.delete(:remove_badge_from_attendee, fn %{badge_redeem: badge_redeem} ->
398+
badge_redeem
399+
end)
400+
|> Multi.update(:attendee_update_entries, fn %{badge_redeem: badge_redeem} ->
401+
Attendee.changeset(badge_redeem.attendee, %{
402+
entries: max(badge_redeem.attendee.entries - badge_redeem.badge.entries, 0)
403+
})
404+
end)
405+
|> Multi.merge(fn %{badge_redeem: badge_redeem} ->
406+
change_attendee_tokens_transaction(
407+
badge_redeem.attendee,
408+
max(badge_redeem.attendee.tokens - badge_redeem.badge.tokens, 0)
409+
)
410+
end)
411+
|> Repo.transaction()
412+
end
413+
379414
@doc """
380415
Creates a daily prize.
381416

lib/safira/contest/badge_redeem.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ defmodule Safira.Contest.BadgeRedeem do
1616
default_limit: 10,
1717
max_limit: 50,
1818
join_fields: [
19-
name: [binding: :user, field: :name]
19+
name: [
20+
binding: :badge,
21+
field: :name
22+
]
2023
]}
24+
2125
schema "badge_redeems" do
2226
belongs_to :badge, Badge
2327

lib/safira_web/components/table.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ defmodule SafiraWeb.Components.Table do
216216
~H"""
217217
<li>
218218
<p class={[
219-
"hover:cursor-default select-none flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 border border-gray-300 bg-gray-100 border-lightShade dark:border-darkShade dark:text-gray-400 dark:bg-dark",
219+
"hover:cursor-default select-none flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 border border-lightShade dark:border-darkShade dark:text-gray-400 dark:bg-dark",
220220
@right_corner && "rounded-e-lg",
221221
@left_corner && "rounded-s-lg"
222222
]}>

lib/safira_web/live/backoffice/attendee_live/index.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule SafiraWeb.Backoffice.AttendeeLive.Index do
1515

1616
@impl true
1717
def handle_params(params, _, socket) do
18-
case Accounts.list_attendees(params) do
18+
case Accounts.list_attendees(params, order_by: [desc: :inserted_at]) do
1919
{:ok, {attendees, meta}} ->
2020
{:noreply,
2121
socket
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
defmodule SafiraWeb.Backoffice.AttendeeLive.RedeemLive.Index do
2+
use SafiraWeb, :live_component
3+
4+
alias Safira.Contest
5+
6+
import SafiraWeb.Components.{Badge, Table, TableSearch}
7+
8+
on_mount {SafiraWeb.StaffRoles, index: %{"badges" => ["revoke"]}}
9+
10+
@limit 5
11+
12+
@impl true
13+
def render(assigns) do
14+
~H"""
15+
<div>
16+
<.page title={@title} subtitle={gettext("Refund badge redeems.")}>
17+
<:actions>
18+
<div class="flex flex-row w-full gap-4">
19+
<.table_search
20+
id="badges-table-name-search"
21+
params={@params}
22+
field={:name}
23+
path={~p"/dashboard/attendees/#{@attendee.id}/redeem"}
24+
placeholder={gettext("Search for badges")}
25+
/>
26+
</div>
27+
</:actions>
28+
<.table id="speakers-table" items={@streams.redeems} meta={@meta} params={@params}>
29+
<:col :let={{_id, redeem}} field={:badge} label="Badge">
30+
<.badge id={redeem.badge.id} badge={redeem.badge} width="max-w-16" />
31+
<div class="flex gap-4 flex-center max-w-16"></div>
32+
</:col>
33+
<:col :let={{_id, redeem}} field={:redeemed_by} label="Redeemed by">
34+
<%= if redeem.redeemed_by, do: redeem.redeemed_by.user.name, else: "System / Company" %>
35+
</:col>
36+
<:col :let={{_id, redeem}} sortable field={:inserted_at} label="Redeemed at">
37+
<%= datetime_to_string(redeem.inserted_at) %>
38+
</:col>
39+
<:action :let={{id, speaker}}>
40+
<div class="flex flex-row gap-2">
41+
<.link
42+
phx-click={
43+
JS.push("delete", value: %{id: speaker.id}, target: @myself) |> hide("##{id}")
44+
}
45+
data-confirm="Are you sure?"
46+
>
47+
<.icon name="hero-trash" class="w-5 h-5" />
48+
</.link>
49+
</div>
50+
</:action>
51+
</.table>
52+
</.page>
53+
</div>
54+
"""
55+
end
56+
57+
@impl true
58+
def mount(socket) do
59+
{:ok, socket}
60+
end
61+
62+
@impl true
63+
def update(assigns, socket) do
64+
case Contest.list_attendee_redeems_meta(
65+
assigns.attendee.id,
66+
Map.put(assigns.params, "page_size", @limit)
67+
) do
68+
{:ok, {redeems, meta}} ->
69+
{:ok,
70+
socket
71+
|> assign(assigns)
72+
|> assign(meta: meta)
73+
|> stream(
74+
:redeems,
75+
redeems,
76+
reset: true
77+
)}
78+
79+
{:error, _error} ->
80+
{:ok, socket}
81+
end
82+
end
83+
84+
@impl true
85+
def handle_event("delete", %{"id" => id}, socket) do
86+
badge_redeem = Contest.get_badge_redeem!(id)
87+
88+
case Contest.revoke_badge_redeem_from_attendee(id) do
89+
{:ok, _} ->
90+
{:noreply, stream_delete(socket, :redeems, badge_redeem)}
91+
92+
{:error, _reason} ->
93+
{:noreply, socket}
94+
end
95+
end
96+
97+
defp datetime_to_string(datetime) do
98+
Timex.format!(datetime, "%D %T", :strftime)
99+
end
100+
end

lib/safira_web/live/backoffice/attendee_live/show.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ defmodule SafiraWeb.Backoffice.AttendeeLive.Show do
1010
{:ok, socket |> assign(:current_page, :attendees)}
1111
end
1212

13-
def handle_params(%{"id" => attendee_id}, _, socket) do
13+
def handle_params(%{"id" => attendee_id} = params, _, socket) do
1414
{:noreply,
1515
socket
16-
|> assign(:attendee, Accounts.get_attendee!(attendee_id, preloads: [:user]))}
16+
|> assign(:attendee, Accounts.get_attendee!(attendee_id, preloads: [:user]))
17+
|> assign(:params, params)}
1718
end
1819
end

lib/safira_web/live/backoffice/attendee_live/show.html.heex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
</.button>
99
</.link>
1010
<.link patch={~p"/dashboard/attendees/#{@attendee.id}/edit/eligibility"}>
11+
<.button>
12+
<.icon name="hero-shield-exclamation" class="w-5 h-5" />
13+
</.button>
14+
</.link>
15+
<.link patch={~p"/dashboard/attendees/#{@attendee.id}/redeem"}>
1116
<.button>
1217
<.icon name="hero-check-badge" class="w-5 h-5" />
1318
</.button>
@@ -99,3 +104,21 @@
99104
patch={~p"/dashboard/attendees/#{@attendee.id}"}
100105
/>
101106
</.modal>
107+
108+
<.modal
109+
:if={@live_action in [:redeem]}
110+
id="attendee-tokens-modal"
111+
show
112+
on_cancel={JS.patch(~p"/dashboard/attendees/#{@attendee.id}")}
113+
>
114+
<.live_component
115+
module={SafiraWeb.Backoffice.AttendeeLive.RedeemLive.Index}
116+
title="Badge redeems"
117+
id={@attendee.id}
118+
current_user={@current_user}
119+
params={@params}
120+
action={@live_action}
121+
attendee={@attendee}
122+
patch={~p"/dashboard/attendees/#{@attendee.id}"}
123+
/>
124+
</.modal>

lib/safira_web/live/backoffice/badge_live/form_component.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ defmodule SafiraWeb.Backoffice.BadgeLive.FormComponent do
3030
<.icon name="hero-check-circle" />
3131
</.button>
3232
</.link>
33+
<.link :if={@badge.id} patch={~p"/dashboard/badges/#{@badge.id}/redeems"}>
34+
<.button>
35+
<.icon name="hero-check-badge" />
36+
</.button>
37+
</.link>
3338
</:actions>
3439
<.simple_form
3540
for={@form}

lib/safira_web/live/backoffice/badge_live/index.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ defmodule SafiraWeb.Backoffice.BadgeLive.Index do
2828

2929
@impl true
3030
def handle_params(params, _url, socket) do
31-
case Contest.list_badges(params) do
31+
badge_params = if socket.assigns.live_action == :index, do: params, else: %{}
32+
33+
case Contest.list_badges(badge_params) do
3234
{:ok, {badges, meta}} ->
3335
{:noreply,
3436
socket
@@ -128,6 +130,16 @@ defmodule SafiraWeb.Backoffice.BadgeLive.Index do
128130
|> assign(:page_title, "Import Badges")
129131
end
130132

133+
defp apply_action(socket, :redeem, %{"id" => id} = params) do
134+
badge = Contest.get_badge!(id)
135+
136+
socket
137+
|> assign(:page_title, "Redeem Badge")
138+
|> assign(:redeem_params, params)
139+
|> assign(:params, %{})
140+
|> assign(:badge, badge)
141+
end
142+
131143
@impl true
132144
def handle_event("delete", %{"id" => id}, socket) do
133145
badge = Contest.get_badge!(id)

lib/safira_web/live/backoffice/badge_live/index.html.heex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,21 @@
179179
patch={~p"/dashboard/badges"}
180180
/>
181181
</.modal>
182+
183+
<.modal
184+
:if={@live_action in [:redeem]}
185+
id="import-modal"
186+
show
187+
on_cancel={JS.patch(~p"/dashboard/badges")}
188+
>
189+
<.live_component
190+
module={SafiraWeb.Backoffice.BadgeLive.RedeemLive.Index}
191+
id="import"
192+
title={@page_title}
193+
current_user={@current_user}
194+
params={@redeem_params}
195+
action={@live_action}
196+
patch={~p"/dashboard/badges"}
197+
badge={@badge}
198+
/>
199+
</.modal>

0 commit comments

Comments
 (0)