diff --git a/lib/safira/accounts/roles/permissions.ex b/lib/safira/accounts/roles/permissions.ex index 1f5c39037..6d5219a51 100644 --- a/lib/safira/accounts/roles/permissions.ex +++ b/lib/safira/accounts/roles/permissions.ex @@ -6,7 +6,7 @@ defmodule Safira.Accounts.Roles.Permissions do def all do %{ - "attendees" => ["show", "edit"], + "attendees" => ["show", "edit", "show_leaderboard"], "staffs" => ["show", "edit", "roles_edit"], "challenges" => ["show", "edit", "delete"], "companies" => ["edit"], diff --git a/lib/safira/inventory/item.ex b/lib/safira/inventory/item.ex index 8349c5ae5..1178e4869 100644 --- a/lib/safira/inventory/item.ex +++ b/lib/safira/inventory/item.ex @@ -6,8 +6,8 @@ defmodule Safira.Inventory.Item do @derive { Flop.Schema, - filterable: [:product_name], - sortable: [:redeemed_at, :inserted_at], + filterable: [:product_name, :prize_name], + sortable: [:redeemed_at, :inserted_at, :product_name], default_limit: 11, join_fields: [ product_name: [ @@ -15,6 +15,12 @@ defmodule Safira.Inventory.Item do field: :name, path: [:product, :name], ecto_type: :string + ], + prize_name: [ + binding: :prize, + field: :name, + path: [:prize, :name], + ecto_type: :string ] ] } diff --git a/lib/safira/store.ex b/lib/safira/store.ex index 74956cf59..73e6c6656 100644 --- a/lib/safira/store.ex +++ b/lib/safira/store.ex @@ -70,6 +70,25 @@ defmodule Safira.Store do |> Flop.validate_and_run(params, for: Item) end + def list_items_type_prize(params) do + Item + |> join(:left, [i], p in assoc(i, :prize), as: :prize) + |> where([i], i.type == :prize and not is_nil(i.prize_id)) + |> where([i], not is_nil(i.product_id)) + |> preload(attendee: [:user], prize: []) + |> Flop.validate_and_run(params, for: Item) + end + + def list_items_type_prize(%{} = params, opts) when is_list(opts) do + Item + |> join(:left, [i], p in assoc(i, :prize), as: :prize) + |> where([i], i.type == :prize) + |> where([i], not is_nil(i.product_id)) + |> preload(attendee: [:user], prize: []) + |> apply_filters(opts) + |> Flop.validate_and_run(params, for: Item) + end + @doc """ Gets a single product. diff --git a/lib/safira_web/live/backoffice/attendee_live/index.html.heex b/lib/safira_web/live/backoffice/attendee_live/index.html.heex index 0b159e21b..1a2bb90bd 100644 --- a/lib/safira_web/live/backoffice/attendee_live/index.html.heex +++ b/lib/safira_web/live/backoffice/attendee_live/index.html.heex @@ -1,6 +1,19 @@ <.page title="Attendees"> <:actions>
+ <.link patch={~p"/dashboard/attendees/leaderboard"}> + <.ensure_permissions + user={@current_user} + permissions={%{"attendees" => ["show_leaderboard"]}} + > + <.button> + + <.icon name="hero-trophy" class="w-5 h-5" /> + + + + + <.link patch={~p"/downloads/attendees"} target="_blank"> <.button> diff --git a/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/day_selector.ex b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/day_selector.ex new file mode 100644 index 000000000..40e855c4f --- /dev/null +++ b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/day_selector.ex @@ -0,0 +1,39 @@ +defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.DaySelector do + @moduledoc """ + Leaderboard component + """ + + use SafiraWeb, :component + + attr :day, :string, required: true + attr :on_left, :any, required: true + attr :on_right, :any, required: true + attr :left_enabled, :boolean, required: true + attr :right_enabled, :boolean, required: true + + def day_selector(assigns) do + ~H""" +
+ +

<%= @day %>

+ +
+ """ + end + + defp enabled_class(enabled) do + if enabled do + "" + else + "opacity-0" + end + end +end diff --git a/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/leaderboard.ex b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/leaderboard.ex new file mode 100644 index 000000000..d7c9c16f7 --- /dev/null +++ b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/components/leaderboard.ex @@ -0,0 +1,109 @@ +defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.Leaderboard do + @moduledoc """ + Leaderboard component + """ + + use SafiraWeb, :component + + import SafiraWeb.Components.Avatar + + attr :entries, :list, required: true + + def leaderboard(assigns) do + ~H""" +
+ <.leaderboard_top_3 entries={Enum.take(@entries, 3)} /> +
    + <.leaderboard_entry :for={entry <- Enum.drop(@entries, 3)} entry={entry} /> +
+
+ """ + end + + defp leaderboard_top_3(assigns) do + ~H""" +
+ <.leaderboard_top_person entry={Enum.at(@entries, 1)} pos={2} /> + <.leaderboard_top_person entry={Enum.at(@entries, 0)} winner={true} pos={1} /> + <.leaderboard_top_person entry={Enum.at(@entries, 2)} pos={3} /> +
+ """ + end + + attr :entry, :map, required: true + attr :winner, :boolean, default: false + attr :pos, :integer, required: true + + defp leaderboard_top_person(assigns) do + ~H""" + <%= if @entry do %> +
+ <.icon + :if={@winner} + name="fa-crown fa-crown-solid" + class="w-10 h-10 translate-y-6 text-accent" + /> + <.avatar + handle={@entry.handle} + size={:xl} + class="bg-light/5 border-2 border-accent bg-accent rounded-full" + link={~p"/dashboard/attendees/#{@entry.attendee_id}"} + /> + + <%= @pos %> + +

<%= @entry.name %>

+

+ <%= gettext("%{badges_count} badges", badges_count: @entry.badges) %> +

+

+ <%= gettext("%{tokens} tokens", tokens: @entry.tokens) %> +

+
+ <% end %> + """ + end + + attr :entry, :map, required: true + + defp leaderboard_entry(assigns) do + ~H""" +
  • +
    +

    + #<%= @entry.position %> +

    +

    + <.avatar + handle={@entry.handle} + size={:sm} + class="bg-light/5 border-2 border-light/5 rounded-full" + link={~p"/app/user/#{@entry.handle}"} + /> +

    + <.link patch={~p"/dashboard/attendees/#{@entry.attendee_id}"}> +

    + <%= @entry.name %> +

    + +
    +
    +

    + + <%= @entry.badges %> + + <%= gettext(" badges") %> + + + + <%= @entry.tokens %> + + <%= gettext(" tokens") %> + + +

    +
    +
  • + """ + end +end diff --git a/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.ex b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.ex new file mode 100644 index 000000000..29d224d40 --- /dev/null +++ b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.ex @@ -0,0 +1,126 @@ +defmodule SafiraWeb.Backoffice.AttendeeLive.LeaderboardLive.Index do + use SafiraWeb, :backoffice_view + + alias Safira.{Contest, Event} + import SafiraWeb.Backoffice.LeaderboardLive.Components.{Leaderboard, DaySelector} + + on_mount {SafiraWeb.StaffRoles, index: %{"attendees" => ["show_leaderboard"]}} + + @limit 30 + + @impl true + def mount(_params, _session, socket) do + daily_prizes = Contest.list_daily_prizes() + + days = Event.list_event_dates() + + start_day_idx = get_start_day_idx(days) + leaderboard = Contest.leaderboard(Enum.at(days, start_day_idx), @limit) + + {:ok, + socket + |> assign(:leaderboard, leaderboard) + |> assign(:current_page, :attendees) + |> assign(:current_day_str, display_current_day(days, start_day_idx)) + |> assign(:current_day_index, start_day_idx) + |> assign(:days, days) + |> assign(:left_enabled, start_day_idx > 0) + |> assign(:right_enabled, start_day_idx < length(days) - 1) + |> assign(:daily_prizes, daily_prizes) + |> assign(:prizes, get_day_prizes(daily_prizes, Enum.at(days, start_day_idx)))} + end + + @impl true + def handle_params(_params, _url, socket) do + {:noreply, socket} + end + + @impl true + def handle_event("on_left", _, socket) do + if socket.assigns.current_day_index > 0 do + day = Enum.at(socket.assigns.days, socket.assigns.current_day_index - 1) + + {:noreply, + socket + |> assign(:current_day_index, socket.assigns.current_day_index - 1) + |> assign( + :current_day_str, + display_current_day(socket.assigns.days, socket.assigns.current_day_index - 1) + ) + |> assign(:left_enabled, socket.assigns.current_day_index > 1) + |> assign(:right_enabled, true) + |> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day)) + |> assign(:leaderboard, Contest.leaderboard(day, @limit))} + else + {:noreply, socket} + end + end + + def handle_event("on_right", _, socket) do + if socket.assigns.current_day_index < length(socket.assigns.days) - 1 do + day = Enum.at(socket.assigns.days, socket.assigns.current_day_index + 1) + + {:noreply, + socket + |> assign(:current_day_index, socket.assigns.current_day_index + 1) + |> assign( + :current_day_str, + display_current_day(socket.assigns.days, socket.assigns.current_day_index + 1) + ) + |> assign(:left_enabled, true) + |> assign( + :right_enabled, + socket.assigns.current_day_index < length(socket.assigns.days) - 2 + ) + |> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day)) + |> assign(:leaderboard, Contest.leaderboard(day, @limit))} + else + {:noreply, socket} + end + end + + defp get_start_day_idx(days) do + today = Date.utc_today() + + idx = Enum.find_index(days, fn d -> d == today end) + + if is_nil(idx) do + 0 + else + idx + end + end + + defp display_current_day(days, index) do + days + |> Enum.at(index) + |> format_date() + end + + defp format_date(date) do + today = Timex.today() + yesterday = Timex.shift(today, days: -1) + + cond do + Timex.equal?(date, today) -> + gettext("Today") + + Timex.equal?(date, yesterday) -> + gettext("Yesterday") + + true -> + Timex.format!(date, "{D} {Mshort}") + end + end + + defp get_day_prizes(daily_prizes, day) do + daily_prizes + |> Enum.filter(fn dp -> + if is_nil(day) do + is_nil(dp.date) + else + dp.date == day + end + end) + end +end diff --git a/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.html.heex b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.html.heex new file mode 100644 index 000000000..211437785 --- /dev/null +++ b/lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.html.heex @@ -0,0 +1,20 @@ +<.page title="Leaderboard"> +
    +
    + <.day_selector + day={@current_day_str} + on_left="on_left" + on_right="on_right" + left_enabled={@left_enabled} + right_enabled={@right_enabled} + /> +
    +
    + <.leaderboard :if={@leaderboard != []} entries={@leaderboard} /> +
    + +

    <%= gettext("No data to show for this day.") %>

    +
    +
    +
    + diff --git a/lib/safira_web/live/backoffice/minigames_live/index.ex b/lib/safira_web/live/backoffice/minigames_live/index.ex index 6d426b1e6..48f79ad85 100644 --- a/lib/safira_web/live/backoffice/minigames_live/index.ex +++ b/lib/safira_web/live/backoffice/minigames_live/index.ex @@ -3,6 +3,7 @@ defmodule SafiraWeb.Backoffice.MinigamesLive.Index do on_mount {SafiraWeb.StaffRoles, index: %{"minigames" => ["show"]}, + show: %{"minigames" => ["show"]}, simulate_wheel: %{"minigames" => ["simulate"]}, edit_wheel_drops: %{"minigames" => ["edit"]}, edit_wheel: %{"minigames" => ["edit"]}, diff --git a/lib/safira_web/live/backoffice/minigames_live/index.html.heex b/lib/safira_web/live/backoffice/minigames_live/index.html.heex index 07651ddee..c4ada6aab 100644 --- a/lib/safira_web/live/backoffice/minigames_live/index.html.heex +++ b/lib/safira_web/live/backoffice/minigames_live/index.html.heex @@ -1,4 +1,14 @@ <.page title="Minigames"> + <:actions> + <.link patch={~p"/dashboard/minigames/drops"}> + <.button> + + <.icon name="hero-trophy" class="w-5 h-5" /> + + + + +
    <.ensure_permissions user={@current_user} permissions={%{"minigames" => ["edit"]}}> <.link diff --git a/lib/safira_web/live/backoffice/minigames_live/show.ex b/lib/safira_web/live/backoffice/minigames_live/show.ex new file mode 100644 index 000000000..cf756217e --- /dev/null +++ b/lib/safira_web/live/backoffice/minigames_live/show.ex @@ -0,0 +1,30 @@ +defmodule SafiraWeb.Backoffice.MinigamesLive.Show do + use SafiraWeb, :backoffice_view + + alias Safira.Store + + import SafiraWeb.Helpers + import SafiraWeb.Components.Table + import SafiraWeb.Components.TableSearch + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(params, _, socket) do + case Store.list_items_type_prize(params) do + {:ok, {items, meta}} -> + {:noreply, + socket + |> assign(:meta, meta) + |> assign(:params, params) + |> assign(:current_page, :show) + |> stream(:items, items, reset: true)} + + {:error, _} -> + {:noreply, socket} + end + end +end diff --git a/lib/safira_web/live/backoffice/minigames_live/show.html.heex b/lib/safira_web/live/backoffice/minigames_live/show.html.heex new file mode 100644 index 000000000..87916bec9 --- /dev/null +++ b/lib/safira_web/live/backoffice/minigames_live/show.html.heex @@ -0,0 +1,57 @@ +<.page title="Drops"> + <:actions> +
    + <.table_search + id="drops-table-attendee-name-search" + params={@params} + field={:prize_name} + path={~p"/dashboard/minigames/drops"} + placeholder={gettext("Search for Prizes")} + /> +
    + +
    + <.table id="items-table" items={@streams.items} meta={@meta} params={@params}> + <:col :let={{_id, item}} label="Attendee" field={:attendee_name}> +
    + <.avatar + handle={item.attendee.user.handle} + src={ + Uploaders.UserPicture.url( + {item.attendee.user.picture, item.attendee.user}, + :original, + signed: true + ) + } + /> +
    +

    <%= item.attendee.user.name %>

    +

    @<%= item.attendee.user.handle %>

    +
    +
    + + <:col :let={{_id, item}} label="Prize" field={:name}> + <%= item.prize.name %> + + <:col :let={{_id, item}} sortable label="Gotten at" field={:inserted_at}> + <%= relative_datetime(item.inserted_at) %> + + <:col :let={{_id, item}} label="Status"> + <%= if item.redeemed_at do %> +

    + <%= gettext("Delivered") %> +

    + <% else %> +

    + <%= gettext("Waiting") %> +

    + <% end %> + + <:action :let={{_id, item}}> +
    + <.link navigate={~p"/dashboard/store/products/#{item}"}>Show +
    + + +
    + diff --git a/lib/safira_web/live/backoffice/prize_live/daily_live/form_component.ex b/lib/safira_web/live/backoffice/prize_live/daily_live/form_component.ex index 37600a82f..b2b023866 100644 --- a/lib/safira_web/live/backoffice/prize_live/daily_live/form_component.ex +++ b/lib/safira_web/live/backoffice/prize_live/daily_live/form_component.ex @@ -1,4 +1,4 @@ -defmodule SafiraWeb.PrizeLive.Daily.FormComponent do +defmodule SafiraWeb.PrizeLive.MinigamesLive.Daily.FormComponent do @moduledoc false use SafiraWeb, :live_component diff --git a/lib/safira_web/live/backoffice/prize_live/form_component.ex b/lib/safira_web/live/backoffice/prize_live/form_component.ex index 02cf22b22..27bf3d33d 100644 --- a/lib/safira_web/live/backoffice/prize_live/form_component.ex +++ b/lib/safira_web/live/backoffice/prize_live/form_component.ex @@ -1,4 +1,4 @@ -defmodule SafiraWeb.PrizeLive.FormComponent do +defmodule SafiraWeb.PrizeLive.MinigamesLive.FormComponent do use SafiraWeb, :live_component alias Safira.Minigames diff --git a/lib/safira_web/live/backoffice/prize_live/index.ex b/lib/safira_web/live/backoffice/prize_live/index.ex index 64adb1337..1b31e7b10 100644 --- a/lib/safira_web/live/backoffice/prize_live/index.ex +++ b/lib/safira_web/live/backoffice/prize_live/index.ex @@ -1,4 +1,4 @@ -defmodule SafiraWeb.Backoffice.PrizeLive.Index do +defmodule SafiraWeb.Backoffice.MinigamesLive.PrizeLive.Index do use SafiraWeb, :backoffice_view alias Safira.Minigames diff --git a/lib/safira_web/router.ex b/lib/safira_web/router.ex index bcc075ca6..18c80d809 100644 --- a/lib/safira_web/router.ex +++ b/lib/safira_web/router.ex @@ -171,6 +171,7 @@ defmodule SafiraWeb.Router do end scope "/attendees", AttendeeLive do + live "/leaderboard", LeaderboardLive.Index, :index live "/", Index, :index live "/:id", Show, :show live "/:id/edit/tokens", Show, :tokens_edit @@ -293,7 +294,10 @@ defmodule SafiraWeb.Router do end end - scope "/minigames" do + scope "/minigames", MinigamesLive do + live "/", Index, :index + live "/drops", Show, :show + scope "/prizes", PrizeLive do live "/", Index, :index live "/new", Index, :new @@ -314,22 +318,20 @@ defmodule SafiraWeb.Router do end scope "/wheel" do - live "/", MinigamesLive.Index, :edit_wheel - live "/drops", MinigamesLive.Index, :edit_wheel_drops - live "/simulator", MinigamesLive.Index, :simulate_wheel + live "/", Index, :edit_wheel + live "/drops", Index, :edit_wheel_drops + live "/simulator", Index, :simulate_wheel end scope "/slots" do - live "/", MinigamesLive.Index, :edit_slots - live "/reels_icons", MinigamesLive.Index, :edit_slots_reel_icons_icons - live "/reels_position", MinigamesLive.Index, :edit_slots_reel_icons_position - live "/paytable", MinigamesLive.Index, :edit_slots_paytable - live "/payline", MinigamesLive.Index, :edit_slots_payline + live "/", Index, :edit_slots + live "/reels_icons", Index, :edit_slots_reel_icons_icons + live "/reels_position", Index, :edit_slots_reel_icons_position + live "/paytable", Index, :edit_slots_paytable + live "/payline", Index, :edit_slots_payline end - live "/", MinigamesLive.Index, :index - - live "/coin_flip", MinigamesLive.Index, :edit_coin_flip + live "/coin_flip", Index, :edit_coin_flip end scope "/scanner", ScannerLive do