From b58ece412d189f418aa5c0f9c4215c5244c7fe1f Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Sun, 23 Nov 2025 15:15:39 +0000 Subject: [PATCH 01/12] feat: tickets & ticket types schemas --- lib/pearl/accounts/user.ex | 4 +++ lib/pearl/tickets.ex | 7 +++++ lib/pearl/tickets/ticket.ex | 30 +++++++++++++++++++ lib/pearl/tickets/ticket_type.ex | 27 +++++++++++++++++ .../20251120141024_add_ticket_types.exs | 14 +++++++++ .../migrations/20251120141058_add_tickets.exs | 17 +++++++++++ 6 files changed, 99 insertions(+) create mode 100644 lib/pearl/tickets.ex create mode 100644 lib/pearl/tickets/ticket.ex create mode 100644 lib/pearl/tickets/ticket_type.ex create mode 100644 priv/repo/migrations/20251120141024_add_ticket_types.exs create mode 100644 priv/repo/migrations/20251120141058_add_tickets.exs diff --git a/lib/pearl/accounts/user.ex b/lib/pearl/accounts/user.ex index f9dba67..f95f248 100644 --- a/lib/pearl/accounts/user.ex +++ b/lib/pearl/accounts/user.ex @@ -35,6 +35,10 @@ defmodule Pearl.Accounts.User do schema "users" do field :name, :string field :email, :string + # field :notes, :string + # field :university, :string + # field :course, :string + # field :code, :string field :handle, :string field :picture, Pearl.Uploaders.UserPicture.Type field :password, :string, virtual: true, redact: true diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex new file mode 100644 index 0000000..fdf9461 --- /dev/null +++ b/lib/pearl/tickets.ex @@ -0,0 +1,7 @@ +defmodule Pearl.Tickets do + @moduledoc """ + The Tickets context. + """ + use Pearl.Context + +end diff --git a/lib/pearl/tickets/ticket.ex b/lib/pearl/tickets/ticket.ex new file mode 100644 index 0000000..029da56 --- /dev/null +++ b/lib/pearl/tickets/ticket.ex @@ -0,0 +1,30 @@ +defmodule Pearl.Tickets.Ticket do + @moduledoc """ + Tickets to access the event. + """ + + use Pearl.Schema + + alias Pearl.Accounts.User + alias Pearl.Tickets.TicketType + + @required_fields ~w(paid user_id ticket_type_id)a + + schema "tickets" do + field :paid, :boolean + + belongs_to :user, User + belongs_to :ticket_type, TicketType + + timestamps(type: :utc_datetime) + end + + def changeset(ticket, attrs) do + ticket + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id) + |> unsafe_validate_unique(:user_id, Pearl.Repo) + end + +end diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex new file mode 100644 index 0000000..7b50cd6 --- /dev/null +++ b/lib/pearl/tickets/ticket_type.ex @@ -0,0 +1,27 @@ +defmodule Pearl.Tickets.TicketType do + + use Pearl.Schema + + alias Pearl.Tickets.Ticket + + @required_fields ~w(name description price)a + @optional_fields ~w(ticket_id)a + + schema "ticket_type" do + field :name, :string + field :description, :string + field :price, :integer + # field :image, Uploaders.Ticket.Type - we don't know if there will be an image related to each ticket at the plataform level + + has_many :ticket, Ticket + + timestamps(type: :utc_datetime) + end + + def changeset(ticket_type, attrs) do + ticket_type + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end + +end diff --git a/priv/repo/migrations/20251120141024_add_ticket_types.exs b/priv/repo/migrations/20251120141024_add_ticket_types.exs new file mode 100644 index 0000000..85d2d85 --- /dev/null +++ b/priv/repo/migrations/20251120141024_add_ticket_types.exs @@ -0,0 +1,14 @@ +defmodule Pearl.Repo.Migrations.AddTicketTypes do + use Ecto.Migration + + def change do + create table(:ticket_types, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :string + add :description, :string + add :price, :integer + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20251120141058_add_tickets.exs b/priv/repo/migrations/20251120141058_add_tickets.exs new file mode 100644 index 0000000..042071d --- /dev/null +++ b/priv/repo/migrations/20251120141058_add_tickets.exs @@ -0,0 +1,17 @@ +defmodule Pearl.Repo.Migrations.AddTickets do + use Ecto.Migration + + def change do + create table(:tickets, primary_key: false) do + add :id, :binary_id, primary_key: true + add :paid, :boolean, null: false + add :user_id, references(:users, type: :binary_id, on_delete: :nothing), null: false + add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :nothing), null: false + + timestamps(type: :utc_datetime) + end + + create unique_index(:tickets, [:user_id]) + create index(:tickets, [:ticket_type_id]) + end +end From 77a6522d3701d345736a5acbb8268e43e73d54ed Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Mon, 24 Nov 2025 14:26:41 +0000 Subject: [PATCH 02/12] feat: add contexts --- lib/pearl/ticket_types.ex | 111 ++++++++++++++++++++++++++++++++++++++ lib/pearl/tickets.ex | 109 +++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 lib/pearl/ticket_types.ex diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex new file mode 100644 index 0000000..2c89b7a --- /dev/null +++ b/lib/pearl/ticket_types.ex @@ -0,0 +1,111 @@ +defmodule Pearl.TicketTypes do + @moduledoc """ + The Ticket Types context + """ + use Pearl.Context + + import Ecto.Query, warn: false + alias Pearl.Repo + + alias Pearl.Tickets.TicketType + + @doc """ + Returns the list of ticket types. + + ## Examples + + iex> list_ticket_types() + [%TicketType{}, ...] + + """ + def list_ticket_types do + Repo.all(TicketType) + end + + + @doc """ + Gets a single ticket type. + + Raises `Ecto.NoResultsError` if the TicketType does not exist. + + ## Examples + + iex> get_ticket_type!(123) + %TicketType{} + + iex> get_ticket_type!(321) + ** (Ecto.NoResultsError) + + """ + + def get_ticket_type!(id) do + TicketType + |> Repo.get!(id) + end + + @doc """ + Creates a ticket type. + + ## Examples + + iex> create_ticket_type(%{field: value}) + {:ok, %TicketType{}} + + iex> create_ticket_type(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_ticket_type(attrs \\ %{}) do + %TicketType{} + |> TicketType.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a ticket type. + + ## Examples + + iex> update_ticket_type(ticket_type, %{field: new_value}) + {:ok, %TicketType{}} + + iex> update_ticket_type(ticket_type, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_ticket_type(%TicketType{} = ticket_type, attrs) do + ticket_type + |> TicketType.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a ticket type. + + ## Examples + + iex> delete_ticket_type(ticket_type) + {:ok, %TicketType{}} + + iex> delete_ticket_type(ticket_type) + {:error, %Ecto.Changeset{}} + + """ + def delete_ticket_type(%TicketType{} = ticket_type) do + Repo.delete(ticket_type) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking ticket types changes. + + ## Examples + + iex> change_ticket_type(ticket_type) + %Ecto.Changeset{data: %TicketType{}} + + """ + def change_ticket_type(%TicketType{} = ticket_type, attrs \\ %{}) do + TicketType.changeset(ticket_type, attrs) + end + +end diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex index fdf9461..b82de2a 100644 --- a/lib/pearl/tickets.ex +++ b/lib/pearl/tickets.ex @@ -4,4 +4,113 @@ defmodule Pearl.Tickets do """ use Pearl.Context + import Ecto.Query, warn: false + alias Pearl.Repo + + alias Pearl.Tickets.Ticket + + @doc """ + Returns the list of tickets. + + ## Examples + + iex> list_tickets() + [%Ticket{}, ...] + + """ + def list_tickets do + Repo.all(Ticket) + end + + def list_tickets(opts) when is_list(opts) do + Ticket + |> apply_filters(opts) + |> Repo.all() + end + + @doc """ + Gets a single ticket. + + Raises `Ecto.NoResultsError` if the Ticket does not exist. + + ## Examples + + iex> get_ticket!(123) + %Ticket{} + + iex> get_ticket!(321) + ** (Ecto.NoResultsError) + + """ + + def get_ticket!(id) do + Ticket + |> preload([:user]) + |> Repo.get!(id) + end + + @doc """ + Creates a ticket. + + ## Examples + + iex> create_ticket(%{field: value}) + {:ok, %Ticket{}} + + iex> create_ticket(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_ticket(attrs \\ %{}) do + %Ticket{} + |> Ticket.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a ticket. + + ## Examples + + iex> update_ticket(ticket, %{field: new_value}) + {:ok, %Ticket{}} + + iex> update_ticket(ticket, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_ticket(%Ticket{} = ticket, attrs) do + ticket + |> Ticket.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a ticket. + + ## Examples + + iex> delete_ticket(ticket) + {:ok, %Ticket{}} + + iex> delete_ticket(ticket) + {:error, %Ecto.Changeset{}} + + """ + def delete_ticket(%Ticket{} = ticket) do + Repo.delete(ticket) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking ticket changes. + + ## Examples + + iex> change_ticket(ticket) + %Ecto.Changeset{data: %Ticket{}} + + """ + def change_ticket(%Ticket{} = ticket, attrs \\ %{}) do + Ticket.changeset(ticket, attrs) + end end From a3b79fca029574f4c17966c0f59f624c623f65fd Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Wed, 26 Nov 2025 19:43:32 +0000 Subject: [PATCH 03/12] feat: add tickets backoffice page --- lib/pearl/accounts/roles/permissions.ex | 1 + lib/pearl/ticket_types.ex | 52 ++++++++- lib/pearl/tickets.ex | 19 +++- lib/pearl/tickets/ticket.ex | 29 ++++- lib/pearl/tickets/ticket_type.ex | 19 ++-- lib/pearl_web/config.ex | 7 ++ .../backoffice/tickets_live/form_component.ex | 97 ++++++++++++++++ .../live/backoffice/tickets_live/index.ex | 64 +++++++++++ .../backoffice/tickets_live/index.html.heex | 105 ++++++++++++++++++ .../ticket_types_live/form_component.ex | 86 ++++++++++++++ .../tickets_live/ticket_types_live/index.ex | 93 ++++++++++++++++ lib/pearl_web/router.ex | 11 ++ .../20251120141024_add_ticket_types.exs | 2 + .../migrations/20251120141058_add_tickets.exs | 4 +- priv/repo/seeds.exs | 3 +- priv/repo/seeds/tickets.exs | 90 +++++++++++++++ 16 files changed, 662 insertions(+), 20 deletions(-) create mode 100644 lib/pearl_web/live/backoffice/tickets_live/form_component.ex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/index.ex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/index.html.heex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex create mode 100644 priv/repo/seeds/tickets.exs diff --git a/lib/pearl/accounts/roles/permissions.ex b/lib/pearl/accounts/roles/permissions.ex index b5d5240..e21fd71 100644 --- a/lib/pearl/accounts/roles/permissions.ex +++ b/lib/pearl/accounts/roles/permissions.ex @@ -10,6 +10,7 @@ defmodule Pearl.Accounts.Roles.Permissions do "staffs" => ["show", "edit", "roles_edit"], "challenges" => ["show", "edit", "delete"], "companies" => ["edit"], + "tickets" => ["edit"], "enrolments" => ["show", "edit"], "products" => ["show", "edit", "delete"], "purchases" => ["show", "redeem", "refund"], diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex index 2c89b7a..66d0427 100644 --- a/lib/pearl/ticket_types.ex +++ b/lib/pearl/ticket_types.ex @@ -19,10 +19,11 @@ defmodule Pearl.TicketTypes do """ def list_ticket_types do - Repo.all(TicketType) + TicketType + |> order_by(:priority) + |> Repo.all() end - @doc """ Gets a single ticket type. @@ -37,10 +38,8 @@ defmodule Pearl.TicketTypes do ** (Ecto.NoResultsError) """ - def get_ticket_type!(id) do - TicketType - |> Repo.get!(id) + Repo.get!(TicketType, id) end @doc """ @@ -95,6 +94,48 @@ defmodule Pearl.TicketTypes do Repo.delete(ticket_type) end + @doc """ + Archives a ticket type. + + iex> archive_ticket_type(ticket_type) + {:ok, %TicketType{}} + + iex> archive_ticket_type(ticket_type) + {:error, %Ecto.Changeset{}} + """ + def archive_ticket_type(%TicketType{} = ticket_type) do + ticket_type + |> TicketType.changeset(%{active: false}) + |> Repo.update() + end + + @doc """ + Unarchives a ticket type. + + iex> unarchive_ticket_type(ticket_type) + {:ok, %TicketType{}} + + iex> unarchive_ticket_type(ticket_type) + {:error, %Ecto.Changeset{}} + """ + def unarchive_ticket_type(%TicketType{} = ticket_type) do + ticket_type + |> TicketType.changeset(%{active: true}) + |> Repo.update() + end + + @doc """ + Returns the next priority a ticket type should have. + + ## Examples + + iex> get_next_ticket_type_priority() + 5 + """ + def get_next_ticket_type_priority do + (Repo.aggregate(from(t in TicketType), :max, :priority) || -1) + 1 + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking ticket types changes. @@ -107,5 +148,4 @@ defmodule Pearl.TicketTypes do def change_ticket_type(%TicketType{} = ticket_type, attrs \\ %{}) do TicketType.changeset(ticket_type, attrs) end - end diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex index b82de2a..fc08298 100644 --- a/lib/pearl/tickets.ex +++ b/lib/pearl/tickets.ex @@ -19,7 +19,8 @@ defmodule Pearl.Tickets do """ def list_tickets do - Repo.all(Ticket) + Ticket + |> Repo.all() end def list_tickets(opts) when is_list(opts) do @@ -28,6 +29,20 @@ defmodule Pearl.Tickets do |> Repo.all() end + def list_tickets(params) do + Ticket + |> join(:left, [t], u in assoc(t, :user), as: :user) + |> join(:left, [t], tt in assoc(t, :ticket_type), as: :ticket_type) + |> preload([user: u, ticket_type: tt], user: u, ticket_type: tt) + |> Flop.validate_and_run(params, for: Ticket) + end + + def list_tickets(%{} = params, opts) when is_list(opts) do + Ticket + |> apply_filters(opts) + |> Flop.validate_and_run(params, for: Ticket) + end + @doc """ Gets a single ticket. @@ -45,7 +60,7 @@ defmodule Pearl.Tickets do def get_ticket!(id) do Ticket - |> preload([:user]) + |> preload([:user, :ticket_type]) |> Repo.get!(id) end diff --git a/lib/pearl/tickets/ticket.ex b/lib/pearl/tickets/ticket.ex index 029da56..52ea106 100644 --- a/lib/pearl/tickets/ticket.ex +++ b/lib/pearl/tickets/ticket.ex @@ -6,15 +6,37 @@ defmodule Pearl.Tickets.Ticket do use Pearl.Schema alias Pearl.Accounts.User + alias Pearl.Repo alias Pearl.Tickets.TicketType + @derive { + Flop.Schema, + filterable: [:paid, :user_name], + sortable: [:paid, :inserted_at, :ticket_type], + default_limit: 11, + join_fields: [ + ticket_type: [ + binding: :ticket_type, + field: :name, + path: [:ticket_type, :name], + ecto_type: :string + ], + user_name: [ + binding: :user, + field: :name, + path: [:user, :name], + ecto_type: :string + ] + ] + } + @required_fields ~w(paid user_id ticket_type_id)a schema "tickets" do field :paid, :boolean belongs_to :user, User - belongs_to :ticket_type, TicketType + belongs_to :ticket_type, TicketType, on_replace: :delete timestamps(type: :utc_datetime) end @@ -24,7 +46,8 @@ defmodule Pearl.Tickets.Ticket do |> cast(attrs, @required_fields) |> validate_required(@required_fields) |> unique_constraint(:user_id) - |> unsafe_validate_unique(:user_id, Pearl.Repo) + |> cast_assoc(:user, with: &User.profile_changeset/2) + |> unsafe_validate_unique(:user_id, Repo) + |> foreign_key_constraint(:ticket_type_id) end - end diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex index 7b50cd6..9eae3ae 100644 --- a/lib/pearl/tickets/ticket_type.ex +++ b/lib/pearl/tickets/ticket_type.ex @@ -1,19 +1,24 @@ defmodule Pearl.Tickets.TicketType do - + @moduledoc """ + Ticket types for Tickets. + """ use Pearl.Schema alias Pearl.Tickets.Ticket - @required_fields ~w(name description price)a - @optional_fields ~w(ticket_id)a + @required_fields ~w(name priority description price active)a + @optional_fields ~w()a + + @derive {Flop.Schema, sortable: [:priority], filterable: []} - schema "ticket_type" do + schema "ticket_types" do field :name, :string + field :priority, :integer field :description, :string field :price, :integer - # field :image, Uploaders.Ticket.Type - we don't know if there will be an image related to each ticket at the plataform level + field :active, :boolean - has_many :ticket, Ticket + has_many :tickets, Ticket timestamps(type: :utc_datetime) end @@ -22,6 +27,6 @@ defmodule Pearl.Tickets.TicketType do ticket_type |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> foreign_key_constraint(:tickets) end - end diff --git a/lib/pearl_web/config.ex b/lib/pearl_web/config.ex index e2b9699..87d732f 100644 --- a/lib/pearl_web/config.ex +++ b/lib/pearl_web/config.ex @@ -153,6 +153,13 @@ defmodule PearlWeb.Config do url: "/dashboard/companies", scope: %{"companies" => ["edit"]} }, + %{ + key: :tickets, + title: "Tickets", + icon: "hero-ticket", + url: "/dashboard/tickets", + scope: %{"tickets" => ["edit"]} + }, %{ key: :store, title: "Store", diff --git a/lib/pearl_web/live/backoffice/tickets_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex new file mode 100644 index 0000000..419b4da --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex @@ -0,0 +1,97 @@ +defmodule PearlWeb.Backoffice.TicketsLive.FormComponent do + use PearlWeb, :live_component + + alias Pearl.Tickets + alias Pearl.TicketTypes + + import PearlWeb.Components.Forms + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + {@title} + <:subtitle> + {gettext("Tickets of the users.")} + + + + <.simple_form + for={@form} + id="ticket-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + autocomplete="off" + > +
+
+ <.field + field={@form[:ticket_type_id]} + type="select" + options={ticket_type_options(@ticket_types)} + label="Ticket Type" + wrapper_class="pr-2" + required + /> + <.field + field={@form[:paid]} + type="checkbox" + label="Paid" + wrapper_class="" + /> +
+
+ <:actions> + <.button phx-disable-with="Saving...">Save Ticket + + +
+ """ + end + + @impl true + def mount(socket) do + {:ok, socket} + end + + @impl true + def update(%{ticket: ticket} = assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign(:ticket, ticket) + |> assign(:ticket_types, TicketTypes.list_ticket_types()) + |> assign_new(:form, fn -> + to_form(Tickets.change_ticket(ticket)) + end)} + end + + @impl true + def handle_event("validate", %{"ticket" => ticket_params}, socket) do + changeset = Tickets.change_ticket(socket.assigns.ticket, ticket_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"ticket" => ticket_params}, socket) do + save_ticket(socket, ticket_params) + end + + defp save_ticket(socket, ticket_params) do + case Tickets.update_ticket(socket.assigns.ticket, ticket_params) do + {:ok, _ticket} -> + {:noreply, + socket + |> put_flash(:info, "Ticket updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp ticket_type_options(ticket_types) do + Enum.map(ticket_types, &{&1.name, &1.id}) + end +end diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/index.ex new file mode 100644 index 0000000..794ca3f --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex @@ -0,0 +1,64 @@ +defmodule PearlWeb.Backoffice.TicketsLive.Index do + use PearlWeb, :backoffice_view + + import PearlWeb.Components.{Table, TableSearch} + + alias Pearl.{Tickets, TicketTypes} + alias Pearl.Tickets.TicketType + + on_mount {PearlWeb.StaffRoles, index: %{"tickets" => ["edit"]}} + + def mount(_params, _session, socket) do + {:ok, socket} + end + + def handle_params(params, _url, socket) do + case Tickets.list_tickets(params) do + {:ok, {tickets, meta}} -> + {:noreply, + socket + |> assign(:meta, meta) + |> assign(:params, params) + |> stream(:tickets, tickets, reset: true) + |> apply_action(socket.assigns.live_action, params)} + + {:error, _} -> + {:noreply, socket} + end + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Tickets") + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Ticket") + |> assign(:ticket, Tickets.get_ticket!(id)) + end + + defp apply_action(socket, :ticket_types, _params) do + socket + |> assign(:page_title, "Listing Ticket Types") + end + + defp apply_action(socket, :ticket_types_edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Ticket Type") + |> assign(:ticket_type, TicketTypes.get_ticket_type!(id)) + end + + defp apply_action(socket, :ticket_types_new, _params) do + socket + |> assign(:page_title, "New Ticket Type") + |> assign(:ticket_type, %TicketType{}) + end + + def handle_event("delete", %{"id" => id}, socket) do + ticket = Tickets.get_ticket!(id) + {:ok, _} = Tickets.delete_ticket(ticket) + + {:noreply, stream_delete(socket, :tickets, ticket)} + end +end diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex new file mode 100644 index 0000000..bd3ae5f --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -0,0 +1,105 @@ +<.page title="Tickets"> + <:actions> +
+ <.table_search + id="ticket-table-name-search" + params={@params} + field={:user_name} + path={~p"/dashboard/tickets"} + placeholder={gettext("Search for users")} + /> + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> + <.link patch={~p"/dashboard/tickets/ticket_types"}> + <.button> + <.icon name="hero-inbox-stack" class="w-5 h-5" /> + + + +
+ + +
+ <.table id="tickets-table" items={@streams.tickets} meta={@meta} params={@params}> + <:col :let={{_id, ticket}} label="User"> + {ticket.user.name} + + <:col :let={{_id, ticket}} sortable field={:paid} label="Paid"> + {if ticket.paid, do: "Yes", else: "No"} + + <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Ticket Type"> + {ticket.ticket_type.name} + <%= if not ticket.ticket_type.active do %> + Inactive + <% end %> + + <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Inserted At"> + {ticket.inserted_at} + + <:action :let={{id, ticket}}> + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> +
+ <.link patch={~p"/dashboard/tickets/#{ticket.id}/edit"}> + <.icon name="hero-pencil" class="w-5 h-5" /> + + <.link + phx-click={JS.push("delete", value: %{id: ticket.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + <.icon name="hero-trash" class="w-5 h-5" /> + +
+ + + +
+ + +<.modal + :if={@live_action in [:new, :edit]} + id="ticket-modal" + show + on_cancel={JS.patch(~p"/dashboard/tickets")} +> + <.live_component + module={PearlWeb.Backoffice.TicketsLive.FormComponent} + id={@ticket.id || :new} + title={@page_title} + current_user={@current_user} + action={@live_action} + ticket={@ticket} + patch={~p"/dashboard/tickets"} + /> + + +<.modal + :if={@live_action in [:ticket_types]} + id="ticket-types-modal" + show + on_cancel={JS.patch(~p"/dashboard/tickets")} +> + <.live_component + module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index} + id="list-ticket-types" + title={@page_title} + current_user={@current_user} + action={@live_action} + patch={~p"/dashboard/tickets"} + /> + + +<.modal + :if={@live_action in [:ticket_types_edit, :ticket_types_new]} + id="ticket-types-form-modal" + show + on_cancel={JS.navigate(~p"/dashboard/tickets/ticket_types")} +> + <.live_component + module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent} + id={@ticket_type.id || :new} + title={@page_title} + current_user={@current_user} + action={@live_action} + ticket_type={@ticket_type} + patch={~p"/dashboard/tickets/ticket_types"} + /> + diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex new file mode 100644 index 0000000..a1e9756 --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex @@ -0,0 +1,86 @@ +defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do + use PearlWeb, :live_component + + alias Pearl.TicketTypes + + import PearlWeb.Components.Forms + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + {@title} + <:subtitle> + {gettext("Ticket types for the event.")} + + + + <.simple_form + for={@form} + id="ticket-type-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.field field={@form[:name]} type="text" label="Name" required /> + <.field field={@form[:description]} type="textarea" label="Description" /> + <.field field={@form[:price]} type="number" label="Price (€)" step="0.01" required /> + <:actions> + <.button phx-disable-with="Saving...">Save Ticket Type + + +
+ """ + end + + @impl true + def update(%{ticket_type: ticket_type} = assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_new(:form, fn -> + to_form(TicketTypes.change_ticket_type(ticket_type)) + end)} + end + + @impl true + def handle_event("validate", %{"ticket_type" => ticket_type_params}, socket) do + changeset = TicketTypes.change_ticket_type(socket.assigns.ticket_type, ticket_type_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"ticket_type" => ticket_type_params}, socket) do + save_ticket_type(socket, socket.assigns.action, ticket_type_params) + end + + defp save_ticket_type(socket, :ticket_types_edit, ticket_type_params) do + case TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params) do + {:ok, _ticket_type} -> + {:noreply, + socket + |> put_flash(:info, "Ticket type updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_ticket_type(socket, :ticket_types_new, ticket_type_params) do + case TicketTypes.create_ticket_type( + ticket_type_params + |> Map.put("priority", TicketTypes.get_next_ticket_type_priority()) + |> Map.put("active", true) + ) do + {:ok, _ticket_type} -> + {:noreply, + socket + |> put_flash(:info, "Ticket type created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex new file mode 100644 index 0000000..b108ef6 --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex @@ -0,0 +1,93 @@ +defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index do + use PearlWeb, :live_component + + alias Pearl.TicketTypes + import PearlWeb.Components.EnsurePermissions + + @impl true + def render(assigns) do + ~H""" +
+ <.page title={@title}> + <:actions> + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> + <.link navigate={~p"/dashboard/tickets/ticket_types/new"}> + <.button>New Ticket Type + + + +
    +
  • ticket_type.id} + class="even:bg-lightShade/20 dark:even:bg-darkShade/20 py-4 px-4 flex flex-row justify-between" + > +
    + <.icon name="hero-bars-3" class="w-5 h-5 handle cursor-pointer ml-4" /> + {ticket_type.name} + <%= if not ticket_type.active do %> + Inactive + <% end %> +
    +

    + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> + <.link navigate={~p"/dashboard/tickets/ticket_types/#{ticket_type.id}/edit"}> + <.icon name="hero-pencil" class="w-5 h-4" /> + + <.link + phx-click={JS.push("toggle_archive", value: %{id: ticket_type.id})} + data-confirm="Are you sure?" + phx-target={@myself} + > + <%= if not ticket_type.active do %> + <.icon name="hero-archive-box-arrow-down" class="w-5 h-5" /> + <% else %> + <.icon name="hero-archive-box" class="w-5 h-5" /> + <% end %> + + +

    +
  • +
+ +
+ """ + end + + @impl true + def mount(socket) do + {:ok, + socket + |> stream(:ticket_types, TicketTypes.list_ticket_types())} + end + + def handle_event("update-sorting", %{"ids" => ids}, socket) do + ids + |> Enum.with_index(0) + |> Enum.each(fn {"ticket_type-" <> id, index} -> + id + |> TicketTypes.get_ticket_type!() + |> TicketTypes.update_ticket_type(%{priority: index}) + end) + + {:noreply, socket} + end + + @impl true + def handle_event("toggle_archive", %{"id" => id}, socket) do + ticket_type = TicketTypes.get_ticket_type!(id) + + if ticket_type.active do + {:ok, _} = TicketTypes.archive_ticket_type(ticket_type) + else + {:ok, _} = TicketTypes.unarchive_ticket_type(ticket_type) + end + + {:noreply, socket |> stream(:ticket_types, TicketTypes.list_ticket_types())} + end +end diff --git a/lib/pearl_web/router.ex b/lib/pearl_web/router.ex index 7c82390..7527444 100644 --- a/lib/pearl_web/router.ex +++ b/lib/pearl_web/router.ex @@ -228,6 +228,17 @@ defmodule PearlWeb.Router do end end + scope "/tickets", TicketsLive do + live "/", Index, :index + live "/:id/edit", Index, :edit + + scope "/ticket_types" do + live "/", Index, :ticket_types + live "/new", Index, :ticket_types_new + live "/:id/edit", Index, :ticket_types_edit + end + end + scope "/schedule", ScheduleLive do live "/edit", Index, :edit_schedule diff --git a/priv/repo/migrations/20251120141024_add_ticket_types.exs b/priv/repo/migrations/20251120141024_add_ticket_types.exs index 85d2d85..be02318 100644 --- a/priv/repo/migrations/20251120141024_add_ticket_types.exs +++ b/priv/repo/migrations/20251120141024_add_ticket_types.exs @@ -4,9 +4,11 @@ defmodule Pearl.Repo.Migrations.AddTicketTypes do def change do create table(:ticket_types, primary_key: false) do add :id, :binary_id, primary_key: true + add :priority, :integer add :name, :string add :description, :string add :price, :integer + add :active, :boolean timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20251120141058_add_tickets.exs b/priv/repo/migrations/20251120141058_add_tickets.exs index 042071d..1ad61e1 100644 --- a/priv/repo/migrations/20251120141058_add_tickets.exs +++ b/priv/repo/migrations/20251120141058_add_tickets.exs @@ -6,7 +6,9 @@ defmodule Pearl.Repo.Migrations.AddTickets do add :id, :binary_id, primary_key: true add :paid, :boolean, null: false add :user_id, references(:users, type: :binary_id, on_delete: :nothing), null: false - add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :nothing), null: false + + add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :nothing), + null: false timestamps(type: :utc_datetime) end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index cf725c6..4c288c8 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,7 +21,8 @@ defmodule Pearl.Repo.Seeds do "companies.exs", "activities.exs", "slots.exs", - "teams.exs" + "teams.exs", + "tickets.exs" ] |> Enum.each(fn file -> Code.require_file("#{@seeds_dir}/#{file}") diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs new file mode 100644 index 0000000..797c496 --- /dev/null +++ b/priv/repo/seeds/tickets.exs @@ -0,0 +1,90 @@ +defmodule Pearl.Repo.Seeds.Tickets do + import Ecto.Query + + alias Pearl.Accounts.User + alias Pearl.{Repo, Tickets, TicketTypes} + alias Pearl.Tickets.{Ticket, TicketType} + + @ticket_types [ + %{name: "Normal", price: 2500, description: "Normal ticket", active: true, priotity: 0}, + %{name: "FullPass", price: 1500, description: "Premium access", active: true, priority: 1}, + %{name: "FullPass+Hotel", price: 5000, description: "Premium access with hotel", active: true, priority: 2}, + %{name: "Student", price: 3000, description: "Discounted ticket for students", active: true, priority: 3}, + %{name: "Early Bird", price: 2000, description: "Discounted early registration", active: true, priority: 4} + ] + + def run do + seed_ticket_types() + seed_tickets() + end + + defp seed_ticket_types do + case Repo.all(TicketType) do + [] -> + Enum.each(@ticket_types, &insert_ticket_type/1) + Mix.shell().info("Seeded ticket types successfully.") + + _ -> + Mix.shell().info("Found ticket types, skipping seeding.") + end + end + + defp insert_ticket_type(attrs) do + case TicketTypes.create_ticket_type(attrs) do + {:ok, _ticket_type} -> + nil + + {:error, _changeset} -> + Mix.shell().error("Failed to insert ticket type: #{attrs.name}") + end + end + + defp seed_tickets do + case Repo.all(Ticket) do + [] -> + users = Repo.all(from u in User, where: u.type == :attendee, limit: 20) + + if Enum.empty?(users) do + Mix.shell().error("No attendee users found. Please create users first.") + else + ticket_types = Repo.all(TicketType) + + empty_ticket_types?(ticket_types) + end + + _ -> + Mix.shell().info("Found tickets, skipping seeding.") + end + end + + defp empty_ticket_types?(ticket_types) do + if Enum.empty?(ticket_types) do + Mix.shell().error("No ticket types found. Please run ticket types seed first.") + else + users + |> Enum.with_index() + |> Enum.each(fn {user, index} -> + ticket_type = Enum.at(ticket_types, rem(index, length(ticket_types))) + + insert_ticket(%{ + user_id: user.id, + ticket_type_id: ticket_type.id, + paid: rem(index, 3) != 0 + }) + end) + + end + end + + defp insert_ticket(attrs) do + case Tickets.create_ticket(attrs) do + {:ok, _ticket} -> + nil + + {:error, changeset} -> + Mix.shell().error("Failed to insert ticket for user #{attrs.user_id}: #{inspect(changeset.errors)}") + end + end +end + +Pearl.Repo.Seeds.Tickets.run() From 4da402e4e90c724392ea1b2b8259d72dbeda0675 Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Thu, 27 Nov 2025 17:19:36 +0000 Subject: [PATCH 04/12] feat: list active types function --- lib/pearl/ticket_types.ex | 17 +++++++++++++++++ .../backoffice/tickets_live/index.html.heex | 4 +++- .../tickets_live/ticket_types_live/index.ex | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex index 66d0427..62c357c 100644 --- a/lib/pearl/ticket_types.ex +++ b/lib/pearl/ticket_types.ex @@ -24,6 +24,23 @@ defmodule Pearl.TicketTypes do |> Repo.all() end + @doc """ + Returns the list of active ticket_types. + + ## Examples + + iex> list_active_ticket_types() + [%TicketType{}, ...] + + """ + + def list_active_ticket_types do + TicketType + |> where([t], t.active == true) + |> order_by(:priority) + |> Repo.all() + end + @doc """ Gets a single ticket type. diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex index bd3ae5f..42e4f16 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -29,7 +29,9 @@ <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Ticket Type"> {ticket.ticket_type.name} <%= if not ticket.ticket_type.active do %> - Inactive + + Inactive + <% end %> <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Inserted At"> diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex index b108ef6..229c5d1 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex @@ -31,7 +31,9 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index do <.icon name="hero-bars-3" class="w-5 h-5 handle cursor-pointer ml-4" /> {ticket_type.name} <%= if not ticket_type.active do %> - Inactive + + Inactive + <% end %>

From 852da31b8f42a40508325a8924ca443a38c0b0da Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Fri, 28 Nov 2025 12:11:05 +0000 Subject: [PATCH 05/12] fix: seeds --- priv/repo/seeds/tickets.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs index 797c496..1b0452c 100644 --- a/priv/repo/seeds/tickets.exs +++ b/priv/repo/seeds/tickets.exs @@ -49,7 +49,7 @@ defmodule Pearl.Repo.Seeds.Tickets do else ticket_types = Repo.all(TicketType) - empty_ticket_types?(ticket_types) + empty_ticket_types?(ticket_types, users) end _ -> @@ -57,7 +57,7 @@ defmodule Pearl.Repo.Seeds.Tickets do end end - defp empty_ticket_types?(ticket_types) do + defp empty_ticket_types?(ticket_types, users) do if Enum.empty?(ticket_types) do Mix.shell().error("No ticket types found. Please run ticket types seed first.") else From b41897990f3284f7e6ed644e78dc0f1111301475 Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Fri, 28 Nov 2025 12:26:29 +0000 Subject: [PATCH 06/12] feat: add discount code table --- lib/pearl/discount_codes.ex | 104 ++++++++++++++++++ lib/pearl/discount_codes/discount_code.ex | 21 ++++ .../20251128122529_create_discount_codes.exs | 14 +++ test/pearl/discount_codes_test.exs | 63 +++++++++++ .../fixtures/discount_codes_fixtures.ex | 22 ++++ 5 files changed, 224 insertions(+) create mode 100644 lib/pearl/discount_codes.ex create mode 100644 lib/pearl/discount_codes/discount_code.ex create mode 100644 priv/repo/migrations/20251128122529_create_discount_codes.exs create mode 100644 test/pearl/discount_codes_test.exs create mode 100644 test/support/fixtures/discount_codes_fixtures.ex diff --git a/lib/pearl/discount_codes.ex b/lib/pearl/discount_codes.ex new file mode 100644 index 0000000..a1b999a --- /dev/null +++ b/lib/pearl/discount_codes.ex @@ -0,0 +1,104 @@ +defmodule Pearl.DiscountCodes do + @moduledoc """ + The DiscountCodes context. + """ + + import Ecto.Query, warn: false + alias Pearl.Repo + + alias Pearl.DiscountCodes.DiscountCode + + @doc """ + Returns the list of discount_codes. + + ## Examples + + iex> list_discount_codes() + [%DiscountCode{}, ...] + + """ + def list_discount_codes do + Repo.all(DiscountCode) + end + + @doc """ + Gets a single discount_code. + + Raises `Ecto.NoResultsError` if the Discount code does not exist. + + ## Examples + + iex> get_discount_code!(123) + %DiscountCode{} + + iex> get_discount_code!(456) + ** (Ecto.NoResultsError) + + """ + def get_discount_code!(id), do: Repo.get!(DiscountCode, id) + + @doc """ + Creates a discount_code. + + ## Examples + + iex> create_discount_code(%{field: value}) + {:ok, %DiscountCode{}} + + iex> create_discount_code(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_discount_code(attrs) do + %DiscountCode{} + |> DiscountCode.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a discount_code. + + ## Examples + + iex> update_discount_code(discount_code, %{field: new_value}) + {:ok, %DiscountCode{}} + + iex> update_discount_code(discount_code, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_discount_code(%DiscountCode{} = discount_code, attrs) do + discount_code + |> DiscountCode.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a discount_code. + + ## Examples + + iex> delete_discount_code(discount_code) + {:ok, %DiscountCode{}} + + iex> delete_discount_code(discount_code) + {:error, %Ecto.Changeset{}} + + """ + def delete_discount_code(%DiscountCode{} = discount_code) do + Repo.delete(discount_code) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking discount_code changes. + + ## Examples + + iex> change_discount_code(discount_code) + %Ecto.Changeset{data: %DiscountCode{}} + + """ + def change_discount_code(%DiscountCode{} = discount_code, attrs \\ %{}) do + DiscountCode.changeset(discount_code, attrs) + end +end diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex new file mode 100644 index 0000000..039cbe4 --- /dev/null +++ b/lib/pearl/discount_codes/discount_code.ex @@ -0,0 +1,21 @@ +defmodule Pearl.DiscountCodes.DiscountCode do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "discount_codes" do + field :code, :string + field :amount, :integer + field :active, :boolean, default: false + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(discount_code, attrs) do + discount_code + |> cast(attrs, [:code, :amount, :active]) + |> validate_required([:code, :amount, :active]) + end +end diff --git a/priv/repo/migrations/20251128122529_create_discount_codes.exs b/priv/repo/migrations/20251128122529_create_discount_codes.exs new file mode 100644 index 0000000..0615acf --- /dev/null +++ b/priv/repo/migrations/20251128122529_create_discount_codes.exs @@ -0,0 +1,14 @@ +defmodule Pearl.Repo.Migrations.CreateDiscountCodes do + use Ecto.Migration + + def change do + create table(:discount_codes, primary_key: false) do + add :id, :binary_id, primary_key: true + add :code, :string + add :amount, :integer + add :active, :boolean, default: false, null: false + + timestamps(type: :utc_datetime) + end + end +end diff --git a/test/pearl/discount_codes_test.exs b/test/pearl/discount_codes_test.exs new file mode 100644 index 0000000..2857a2b --- /dev/null +++ b/test/pearl/discount_codes_test.exs @@ -0,0 +1,63 @@ +defmodule Pearl.DiscountCodesTest do + use Pearl.DataCase + + alias Pearl.DiscountCodes + + describe "discount_codes" do + alias Pearl.DiscountCodes.DiscountCode + + import Pearl.DiscountCodesFixtures + + @invalid_attrs %{active: nil, code: nil, amount: nil} + + test "list_discount_codes/0 returns all discount_codes" do + discount_code = discount_code_fixture() + assert DiscountCodes.list_discount_codes() == [discount_code] + end + + test "get_discount_code!/1 returns the discount_code with given id" do + discount_code = discount_code_fixture() + assert DiscountCodes.get_discount_code!(discount_code.id) == discount_code + end + + test "create_discount_code/1 with valid data creates a discount_code" do + valid_attrs = %{active: true, code: "some code", amount: 42} + + assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.create_discount_code(valid_attrs) + assert discount_code.active == true + assert discount_code.code == "some code" + assert discount_code.amount == 42 + end + + test "create_discount_code/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = DiscountCodes.create_discount_code(@invalid_attrs) + end + + test "update_discount_code/2 with valid data updates the discount_code" do + discount_code = discount_code_fixture() + update_attrs = %{active: false, code: "some updated code", amount: 43} + + assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.update_discount_code(discount_code, update_attrs) + assert discount_code.active == false + assert discount_code.code == "some updated code" + assert discount_code.amount == 43 + end + + test "update_discount_code/2 with invalid data returns error changeset" do + discount_code = discount_code_fixture() + assert {:error, %Ecto.Changeset{}} = DiscountCodes.update_discount_code(discount_code, @invalid_attrs) + assert discount_code == DiscountCodes.get_discount_code!(discount_code.id) + end + + test "delete_discount_code/1 deletes the discount_code" do + discount_code = discount_code_fixture() + assert {:ok, %DiscountCode{}} = DiscountCodes.delete_discount_code(discount_code) + assert_raise Ecto.NoResultsError, fn -> DiscountCodes.get_discount_code!(discount_code.id) end + end + + test "change_discount_code/1 returns a discount_code changeset" do + discount_code = discount_code_fixture() + assert %Ecto.Changeset{} = DiscountCodes.change_discount_code(discount_code) + end + end +end diff --git a/test/support/fixtures/discount_codes_fixtures.ex b/test/support/fixtures/discount_codes_fixtures.ex new file mode 100644 index 0000000..0734205 --- /dev/null +++ b/test/support/fixtures/discount_codes_fixtures.ex @@ -0,0 +1,22 @@ +defmodule Pearl.DiscountCodesFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Pearl.DiscountCodes` context. + """ + + @doc """ + Generate a discount_code. + """ + def discount_code_fixture(attrs \\ %{}) do + {:ok, discount_code} = + attrs + |> Enum.into(%{ + active: true, + amount: 42, + code: "some code" + }) + |> Pearl.DiscountCodes.create_discount_code() + + discount_code + end +end From a06c06076e2fd0375ca509a07567131c5f839b56 Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Sun, 30 Nov 2025 20:01:49 +0000 Subject: [PATCH 07/12] feat: backoffice for discount codes --- lib/pearl/accounts/roles/permissions.ex | 1 + lib/pearl/discount_codes.ex | 14 +- lib/pearl/discount_codes/discount_code.ex | 40 ++++- lib/pearl/tickets/ticket_type.ex | 5 + lib/pearl_web/config.ex | 7 + .../discount_codes_live/form_component.ex | 150 ++++++++++++++++++ .../backoffice/discount_codes_live/index.ex | 54 +++++++ .../discount_codes_live/index.html.heex | 83 ++++++++++ .../live/backoffice/tickets_live/index.ex | 1 + .../backoffice/tickets_live/index.html.heex | 3 - lib/pearl_web/router.ex | 6 + ...926_create_discount_codes_ticket_types.exs | 17 ++ priv/repo/seeds.exs | 3 +- priv/repo/seeds/discount_codes.exs | 71 +++++++++ test/pearl/discount_codes_test.exs | 18 ++- 15 files changed, 460 insertions(+), 13 deletions(-) create mode 100644 lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex create mode 100644 lib/pearl_web/live/backoffice/discount_codes_live/index.ex create mode 100644 lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex create mode 100644 priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs create mode 100644 priv/repo/seeds/discount_codes.exs diff --git a/lib/pearl/accounts/roles/permissions.ex b/lib/pearl/accounts/roles/permissions.ex index e21fd71..b920541 100644 --- a/lib/pearl/accounts/roles/permissions.ex +++ b/lib/pearl/accounts/roles/permissions.ex @@ -11,6 +11,7 @@ defmodule Pearl.Accounts.Roles.Permissions do "challenges" => ["show", "edit", "delete"], "companies" => ["edit"], "tickets" => ["edit"], + "discount_codes" => ["edit"], "enrolments" => ["show", "edit"], "products" => ["show", "edit", "delete"], "purchases" => ["show", "redeem", "refund"], diff --git a/lib/pearl/discount_codes.ex b/lib/pearl/discount_codes.ex index a1b999a..de79b5d 100644 --- a/lib/pearl/discount_codes.ex +++ b/lib/pearl/discount_codes.ex @@ -17,8 +17,10 @@ defmodule Pearl.DiscountCodes do [%DiscountCode{}, ...] """ - def list_discount_codes do - Repo.all(DiscountCode) + def list_discount_codes(params \\ %{}) do + DiscountCode + |> preload(:ticket_types) + |> Flop.validate_and_run(params, for: DiscountCode) end @doc """ @@ -35,7 +37,11 @@ defmodule Pearl.DiscountCodes do ** (Ecto.NoResultsError) """ - def get_discount_code!(id), do: Repo.get!(DiscountCode, id) + def get_discount_code!(id) do + DiscountCode + |> Repo.get!(id) + |> Repo.preload(:ticket_types) + end @doc """ Creates a discount_code. @@ -51,6 +57,7 @@ defmodule Pearl.DiscountCodes do """ def create_discount_code(attrs) do %DiscountCode{} + |> Repo.preload(:ticket_types) |> DiscountCode.changeset(attrs) |> Repo.insert() end @@ -69,6 +76,7 @@ defmodule Pearl.DiscountCodes do """ def update_discount_code(%DiscountCode{} = discount_code, attrs) do discount_code + |> Repo.preload(:ticket_types) |> DiscountCode.changeset(attrs) |> Repo.update() end diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex index 039cbe4..0672a7f 100644 --- a/lib/pearl/discount_codes/discount_code.ex +++ b/lib/pearl/discount_codes/discount_code.ex @@ -2,20 +2,56 @@ defmodule Pearl.DiscountCodes.DiscountCode do use Ecto.Schema import Ecto.Changeset + alias Pearl.Tickets.TicketType + + @derive { + Flop.Schema, + filterable: [:code, :active], + sortable: [:code, :amount, :active, :inserted_at], + default_limit: 25 + } + + @required_fields ~w(code amount active)a + @optional_fields ~w()a + @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id + schema "discount_codes" do field :code, :string field :amount, :integer field :active, :boolean, default: false + many_to_many :ticket_types, TicketType, join_through: "discount_codes_ticket_types", on_replace: :delete + timestamps(type: :utc_datetime) end @doc false def changeset(discount_code, attrs) do discount_code - |> cast(attrs, [:code, :amount, :active]) - |> validate_required([:code, :amount, :active]) + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> put_ticket_types(attrs) + end + + defp put_ticket_types(changeset, %{ticket_type_ids: ticket_type_ids}) when is_list(ticket_type_ids) do + ticket_types = + ticket_type_ids + |> Enum.reject(&(&1 == "" or is_nil(&1))) + |> Enum.map(&Pearl.TicketTypes.get_ticket_type!/1) + + put_assoc(changeset, :ticket_types, ticket_types) + end + + defp put_ticket_types(changeset, %{"ticket_type_ids" => ticket_type_ids}) when is_list(ticket_type_ids) do + ticket_types = + ticket_type_ids + |> Enum.reject(&(&1 == "" or is_nil(&1))) + |> Enum.map(&Pearl.TicketTypes.get_ticket_type!/1) + + put_assoc(changeset, :ticket_types, ticket_types) end + + defp put_ticket_types(changeset, _attrs), do: changeset end diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex index 9eae3ae..309332b 100644 --- a/lib/pearl/tickets/ticket_type.ex +++ b/lib/pearl/tickets/ticket_type.ex @@ -5,6 +5,7 @@ defmodule Pearl.Tickets.TicketType do use Pearl.Schema alias Pearl.Tickets.Ticket + alias Pearl.DiscountCodes.DiscountCode @required_fields ~w(name priority description price active)a @optional_fields ~w()a @@ -20,6 +21,10 @@ defmodule Pearl.Tickets.TicketType do has_many :tickets, Ticket + many_to_many :discount_codes, DiscountCode, + join_through: "discount_codes_ticket_types", + on_replace: :delete + timestamps(type: :utc_datetime) end diff --git a/lib/pearl_web/config.ex b/lib/pearl_web/config.ex index 87d732f..a01c883 100644 --- a/lib/pearl_web/config.ex +++ b/lib/pearl_web/config.ex @@ -160,6 +160,13 @@ defmodule PearlWeb.Config do url: "/dashboard/tickets", scope: %{"tickets" => ["edit"]} }, + %{ + key: :discount_codes, + title: "Discount Codes", + icon: "hero-tag", + url: "/dashboard/discount_codes", + scope: %{"discount_codes" => ["edit"]} + }, %{ key: :store, title: "Store", diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex new file mode 100644 index 0000000..8819a9d --- /dev/null +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -0,0 +1,150 @@ +defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do + use PearlWeb, :live_component + + alias Pearl.DiscountCodes + alias Pearl.TicketTypes + + import PearlWeb.Components.Forms + + @impl true + def render(assigns) do + ~H""" +

+ <.header> + {@title} + <:subtitle> + {gettext("Manage discount codes for tickets.")} + + + + <.simple_form + for={@form} + id="discount-code-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + autocomplete="off" + > + <.field + field={@form[:code]} + type="text" + label="Code" + required + /> + + <.field + field={@form[:amount]} + type="number" + label="Discount (%)" + required + /> + + <.field + field={@form[:active]} + type="checkbox" + label="Active" + /> + +
+ +
+ <%= for ticket_type <- @ticket_types do %> + + <% end %> +
+ +
+ + <:actions> + <.button phx-disable-with="Saving...">Save Discount Code + + +
+ """ + end + + @impl true + def mount(socket) do + {:ok, socket} + end + + @impl true + def update(%{discount_code: discount_code} = assigns, socket) do + ticket_types = TicketTypes.list_ticket_types() + + selected_ids = + case discount_code.ticket_types do + %Ecto.Association.NotLoaded{} -> [] + ticket_types when is_list(ticket_types) -> Enum.map(ticket_types, & &1.id) + _ -> [] + end + + {:ok, + socket + |> assign(assigns) + |> assign(:ticket_types, ticket_types) + |> assign(:selected_ticket_type_ids, selected_ids) + |> assign_new(:form, fn -> + to_form(DiscountCodes.change_discount_code(discount_code)) + end)} + end + + @impl true + def handle_event("validate", %{"discount_code" => discount_code_params}, socket) do + changeset = DiscountCodes.change_discount_code(socket.assigns.discount_code, discount_code_params) + + selected_ids = + case discount_code_params do + %{"ticket_type_ids" => ids} when is_list(ids) -> + ids |> Enum.reject(&(&1 == "" or is_nil(&1))) + _ -> + [] + end + + {:noreply, + socket + |> assign(form: to_form(changeset, action: :validate)) + |> assign(selected_ticket_type_ids: selected_ids)} + end + + def handle_event("save", %{"discount_code" => discount_code_params}, socket) do + save_discount_code(socket, socket.assigns.action, discount_code_params) + end + + defp save_discount_code(socket, :edit, discount_code_params) do + case DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params) do + {:ok, _discount_code} -> + {:noreply, + socket + |> put_flash(:info, "Discount code updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_discount_code(socket, :new, discount_code_params) do + case DiscountCodes.create_discount_code(discount_code_params) do + {:ok, _discount_code} -> + {:noreply, + socket + |> put_flash(:info, "Discount code created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.ex b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex new file mode 100644 index 0000000..9bfe325 --- /dev/null +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex @@ -0,0 +1,54 @@ +defmodule PearlWeb.Backoffice.DiscountCodesLive.Index do + use PearlWeb, :backoffice_view + + import PearlWeb.Components.{Table, TableSearch} + + alias Pearl.DiscountCodes + alias Pearl.DiscountCodes.DiscountCode + + on_mount {PearlWeb.StaffRoles, index: %{"discount_codes" => ["edit"]}} + + def mount(_params, _session, socket) do + {:ok, socket} + end + + def handle_params(params, _url, socket) do + case DiscountCodes.list_discount_codes(params) do + {:ok, {discount_codes, meta}} -> + {:noreply, + socket + |> assign(:current_page, :discount_codes) + |> assign(:meta, meta) + |> assign(:params, params) + |> stream(:discount_codes, discount_codes, reset: true) + |> apply_action(socket.assigns.live_action, params)} + + {:error, _} -> + {:noreply, socket} + end + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Discount Codes") + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Discount Code") + |> assign(:discount_code, %DiscountCode{}) + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Discount Code") + |> assign(:discount_code, DiscountCodes.get_discount_code!(id)) + end + + def handle_event("delete", %{"id" => id}, socket) do + discount_code = DiscountCodes.get_discount_code!(id) + {:ok, _} = DiscountCodes.delete_discount_code(discount_code) + + {:noreply, stream_delete(socket, :discount_codes, discount_code)} + end +end diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex new file mode 100644 index 0000000..7a30000 --- /dev/null +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -0,0 +1,83 @@ +<.page title="Discount Codes"> + <:actions> +
+ <.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}> + <.link patch={~p"/dashboard/discount_codes/new"}> + <.button> + New Discount + + + +
+ + +
+ <.table + id="discount_codes-table" + items={@streams.discount_codes} + meta={@meta} + params={@params} + > + <:col :let={{_id, discount_code}} field={:code} label="Code"> + {discount_code.code} + + <:col :let={{_id, discount_code}} sortable field={:amount} label="Amount (%)"> + {discount_code.amount} + + <:col :let={{_id, discount_code}} sortable field={:active} label="Active"> + {discount_code.active} + + <:col :let={{_id, discount_code}} field={:ticket_types} label="Ticket Types"> +
+ <%= for ticket_type <- discount_code.ticket_types do %> + + {ticket_type.name} + + <% end %> + <%= if Enum.empty?(discount_code.ticket_types) do %> + No ticket types + <% end %> +
+ + <:action :let={{id, discount_code}}> + <.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}> +
+ <.link patch={~p"/dashboard/discount_codes/#{discount_code.id}/edit"}> + <.icon name="hero-pencil" class="w-5 h-5" /> + + <.link + phx-click={JS.push("delete", value: %{id: discount_code.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + <.icon name="hero-trash" class="w-5 h-5" /> + +
+ + + +
+ + +<.modal + :if={@live_action in [:new, :edit]} + id="discount_code-modal" + show + on_cancel={JS.patch(~p"/dashboard/discount_codes")} +> + <.live_component + module={PearlWeb.Backoffice.DiscountCodesLive.FormComponent} + id={@discount_code.id || :new} + title={@page_title} + current_user={@current_user} + action={@live_action} + discount_code={@discount_code} + patch={~p"/dashboard/discount_codes"} + /> + \ No newline at end of file diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/index.ex index 794ca3f..b2035b9 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex @@ -17,6 +17,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.Index do {:ok, {tickets, meta}} -> {:noreply, socket + |> assign(:current_page, :tickets) |> assign(:meta, meta) |> assign(:params, params) |> stream(:tickets, tickets, reset: true) diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex index 42e4f16..330a305 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -34,9 +34,6 @@ <% end %> - <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Inserted At"> - {ticket.inserted_at} - <:action :let={{id, ticket}}> <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
diff --git a/lib/pearl_web/router.ex b/lib/pearl_web/router.ex index 7527444..0353ebf 100644 --- a/lib/pearl_web/router.ex +++ b/lib/pearl_web/router.ex @@ -239,6 +239,12 @@ defmodule PearlWeb.Router do end end + scope "/discount_codes", DiscountCodesLive do + live "/", Index, :index + live "/new", Index, :new + live "/:id/edit", Index, :edit + end + scope "/schedule", ScheduleLive do live "/edit", Index, :edit_schedule diff --git a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs new file mode 100644 index 0000000..723d9a1 --- /dev/null +++ b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs @@ -0,0 +1,17 @@ +defmodule Pearl.Repo.Migrations.CreateDiscountCodesTicketTypes do + use Ecto.Migration + + def change do + create table(:discount_codes_ticket_types, primary_key: false) do + add :discount_code_id, + references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false + + add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :delete_all), + null: false + end + + create index(:discount_codes_ticket_types, [:discount_code_id]) + create index(:discount_codes_ticket_types, [:ticket_type_id]) + create unique_index(:discount_codes_ticket_types, [:discount_code_id, :ticket_type_id]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 4c288c8..6309f3d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -22,7 +22,8 @@ defmodule Pearl.Repo.Seeds do "activities.exs", "slots.exs", "teams.exs", - "tickets.exs" + "tickets.exs", + "discount_codes.exs" ] |> Enum.each(fn file -> Code.require_file("#{@seeds_dir}/#{file}") diff --git a/priv/repo/seeds/discount_codes.exs b/priv/repo/seeds/discount_codes.exs new file mode 100644 index 0000000..d6f88bf --- /dev/null +++ b/priv/repo/seeds/discount_codes.exs @@ -0,0 +1,71 @@ +alias Pearl.Repo +alias Pearl.DiscountCodes.DiscountCode +alias Pearl.Tickets.TicketType + +ticket_types = Repo.all(TicketType) + +if Enum.empty?(ticket_types) do + IO.puts("No ticket types found. Please run ticket_types seeds first.") +else + normal = Enum.find(ticket_types, &(&1.name == "Normal")) + fullpass = Enum.find(ticket_types, &(&1.name == "FullPass")) + fullpass_hotel = Enum.find(ticket_types, &(&1.name == "FullPass+Hotel")) + student = Enum.find(ticket_types, &(&1.name == "Student")) + early_bird = Enum.find(ticket_types, &(&1.name == "Early Bird")) + + discount_codes = [ + %{ + code: "EARLYBIRD2025", + amount: 10, + active: true, + ticket_type_ids: [normal, early_bird] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + }, + %{ + code: "STUDENT50", + amount: 20, + active: true, + ticket_type_ids: [student] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + }, + %{ + code: "FULLPASS20", + amount: 100, + active: true, + ticket_type_ids: [fullpass, fullpass_hotel] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + }, + %{ + code: "ALLACCESS", + amount: 100, + active: true, + ticket_type_ids: ticket_types |> Enum.map(& &1.id) + }, + %{ + code: "SPONSOR25", + amount: 100, + active: true, + ticket_type_ids: [normal, fullpass] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + }, + %{ + code: "EXPIRED2024", + amount: 33, + active: false, + ticket_type_ids: [normal] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + } + ] + + Enum.each(discount_codes, fn attrs -> + case Repo.get_by(DiscountCode, code: attrs.code) do + nil -> + case Pearl.DiscountCodes.create_discount_code(attrs) do + {:ok, _} -> + IO.puts("Created discount code: #{attrs.code}") + {:error, changeset} -> + IO.puts("Failed to create discount code #{attrs.code}: #{inspect(changeset.errors)}") + end + + existing -> + IO.puts("Discount code already exists: #{existing.code}") + end + end) + + IO.puts("Discount codes seeded successfully!") +end diff --git a/test/pearl/discount_codes_test.exs b/test/pearl/discount_codes_test.exs index 2857a2b..b856803 100644 --- a/test/pearl/discount_codes_test.exs +++ b/test/pearl/discount_codes_test.exs @@ -23,7 +23,9 @@ defmodule Pearl.DiscountCodesTest do test "create_discount_code/1 with valid data creates a discount_code" do valid_attrs = %{active: true, code: "some code", amount: 42} - assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.create_discount_code(valid_attrs) + assert {:ok, %DiscountCode{} = discount_code} = + DiscountCodes.create_discount_code(valid_attrs) + assert discount_code.active == true assert discount_code.code == "some code" assert discount_code.amount == 42 @@ -37,7 +39,9 @@ defmodule Pearl.DiscountCodesTest do discount_code = discount_code_fixture() update_attrs = %{active: false, code: "some updated code", amount: 43} - assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.update_discount_code(discount_code, update_attrs) + assert {:ok, %DiscountCode{} = discount_code} = + DiscountCodes.update_discount_code(discount_code, update_attrs) + assert discount_code.active == false assert discount_code.code == "some updated code" assert discount_code.amount == 43 @@ -45,14 +49,20 @@ defmodule Pearl.DiscountCodesTest do test "update_discount_code/2 with invalid data returns error changeset" do discount_code = discount_code_fixture() - assert {:error, %Ecto.Changeset{}} = DiscountCodes.update_discount_code(discount_code, @invalid_attrs) + + assert {:error, %Ecto.Changeset{}} = + DiscountCodes.update_discount_code(discount_code, @invalid_attrs) + assert discount_code == DiscountCodes.get_discount_code!(discount_code.id) end test "delete_discount_code/1 deletes the discount_code" do discount_code = discount_code_fixture() assert {:ok, %DiscountCode{}} = DiscountCodes.delete_discount_code(discount_code) - assert_raise Ecto.NoResultsError, fn -> DiscountCodes.get_discount_code!(discount_code.id) end + + assert_raise Ecto.NoResultsError, fn -> + DiscountCodes.get_discount_code!(discount_code.id) + end end test "change_discount_code/1 returns a discount_code changeset" do From fb0d5c2be54ffbeca7c78e2a5f163b6228742372 Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Wed, 3 Dec 2025 10:01:11 +0000 Subject: [PATCH 08/12] feat: add perks and some fixed --- lib/pearl/discount_codes.ex | 26 +++++ lib/pearl/discount_codes/discount_code.ex | 27 ++---- lib/pearl/perks.ex | 45 +++++++++ lib/pearl/ticket_types.ex | 31 +++++- lib/pearl/tickets.ex | 96 +++++++++++++++++++ lib/pearl/tickets/perk.ex | 32 +++++++ lib/pearl/tickets/ticket_type.ex | 17 +++- lib/pearl_web/components/core_components.ex | 2 +- .../discount_codes_live/form_component.ex | 32 ++++--- .../backoffice/discount_codes_live/index.ex | 2 +- .../discount_codes_live/index.html.heex | 2 +- .../live/backoffice/tickets_live/index.ex | 21 +++- .../backoffice/tickets_live/index.html.heex | 37 ++++++- .../ticket_types_live/form_component.ex | 87 +++++++++++++---- .../tickets_live/ticket_types_live/index.ex | 3 + .../perks_live/form_component.ex | 86 +++++++++++++++++ .../ticket_types_live/perks_live/index.ex | 81 ++++++++++++++++ lib/pearl_web/router.ex | 6 ++ .../20251120141024_add_ticket_types.exs | 4 +- .../20251128122529_create_discount_codes.exs | 3 +- ...926_create_discount_codes_ticket_types.exs | 5 +- .../20251202172259_create_perks.exs | 16 ++++ .../20251202172629_create_jones.exs | 20 ++++ priv/repo/seeds/tickets.exs | 81 +++++++++++----- priv/static/images/starts.svg | 4 +- test/pearl/tickets_test.exs | 65 +++++++++++++ test/support/fixtures/tickets_fixtures.ex | 23 +++++ 27 files changed, 762 insertions(+), 92 deletions(-) create mode 100644 lib/pearl/perks.ex create mode 100644 lib/pearl/tickets/perk.ex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex create mode 100644 lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex create mode 100644 priv/repo/migrations/20251202172259_create_perks.exs create mode 100644 priv/repo/migrations/20251202172629_create_jones.exs create mode 100644 test/pearl/tickets_test.exs create mode 100644 test/support/fixtures/tickets_fixtures.ex diff --git a/lib/pearl/discount_codes.ex b/lib/pearl/discount_codes.ex index de79b5d..8bab0a3 100644 --- a/lib/pearl/discount_codes.ex +++ b/lib/pearl/discount_codes.ex @@ -109,4 +109,30 @@ defmodule Pearl.DiscountCodes do def change_discount_code(%DiscountCode{} = discount_code, attrs \\ %{}) do DiscountCode.changeset(discount_code, attrs) end + + @doc """ + Updates a discount code's ticket types. + + ## Examples + + iex> upsert_discount_code_ticket_types(discount_code, ["id1", "id2"]) + {:ok, %DiscountCode{}} + + iex> upsert_discount_code_ticket_types(discount_code, ["id1", "id2"]) + {:error, %Ecto.Changeset{}} + + """ + def upsert_discount_code_ticket_types(%DiscountCode{} = discount_code, ticket_type_ids) do + ids = ticket_type_ids || [] + + ticket_types = + Pearl.Tickets.TicketType + |> where([t], t.id in ^ids) + |> Repo.all() + + discount_code + |> DiscountCode.changeset_update_ticket_types(ticket_types) + |> Repo.update() + end + end diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex index 0672a7f..e050983 100644 --- a/lib/pearl/discount_codes/discount_code.ex +++ b/lib/pearl/discount_codes/discount_code.ex @@ -19,8 +19,9 @@ defmodule Pearl.DiscountCodes.DiscountCode do schema "discount_codes" do field :code, :string - field :amount, :integer + field :amount, :float field :active, :boolean, default: false + field :usage_limit, :integer many_to_many :ticket_types, TicketType, join_through: "discount_codes_ticket_types", on_replace: :delete @@ -32,26 +33,12 @@ defmodule Pearl.DiscountCodes.DiscountCode do discount_code |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> put_ticket_types(attrs) end - defp put_ticket_types(changeset, %{ticket_type_ids: ticket_type_ids}) when is_list(ticket_type_ids) do - ticket_types = - ticket_type_ids - |> Enum.reject(&(&1 == "" or is_nil(&1))) - |> Enum.map(&Pearl.TicketTypes.get_ticket_type!/1) - - put_assoc(changeset, :ticket_types, ticket_types) - end - - defp put_ticket_types(changeset, %{"ticket_type_ids" => ticket_type_ids}) when is_list(ticket_type_ids) do - ticket_types = - ticket_type_ids - |> Enum.reject(&(&1 == "" or is_nil(&1))) - |> Enum.map(&Pearl.TicketTypes.get_ticket_type!/1) - - put_assoc(changeset, :ticket_types, ticket_types) + @doc false + def changeset_update_ticket_types(discount_code, ticket_types) do + discount_code + |> cast(%{}, @required_fields ++ @optional_fields) + |> put_assoc(:ticket_types, ticket_types) end - - defp put_ticket_types(changeset, _attrs), do: changeset end diff --git a/lib/pearl/perks.ex b/lib/pearl/perks.ex new file mode 100644 index 0000000..779f471 --- /dev/null +++ b/lib/pearl/perks.ex @@ -0,0 +1,45 @@ +defmodule Pearl.Perks do + @moduledoc """ + Context for Perks + """ + use Pearl.Context + alias Pearl.Tickets.Perk + + def list_perks() do + Repo.all(Perk) + end + + def get_perk!(id) do + Perk + |> Repo.get!(id) + end + + def create_perk(attrs \\ %{}) do + %Perk{} + |> Perk.changeset(attrs) + |> Repo.insert() + end + + def change_perk(%Perk{} = perk, attrs \\ %{}) do + Perk.changeset(perk, attrs) + end + + def update_perk(%Perk{} = perk, attrs) do + perk + |> Perk.changeset(attrs) + |> Repo.update() + end + + def archive_perk(%Perk{} = perk) do + perk + |> Perk.changeset(%{active: false}) + |> Repo.update() + end + + def unarchive_perk(%Perk{} = perk) do + perk + |> Perk.changeset(%{active: true}) + |> Repo.update() + end + +end diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex index 62c357c..22c631b 100644 --- a/lib/pearl/ticket_types.ex +++ b/lib/pearl/ticket_types.ex @@ -56,7 +56,9 @@ defmodule Pearl.TicketTypes do """ def get_ticket_type!(id) do - Repo.get!(TicketType, id) + TicketType + |> Repo.get!(id) + |> Repo.preload(:perks) end @doc """ @@ -165,4 +167,31 @@ defmodule Pearl.TicketTypes do def change_ticket_type(%TicketType{} = ticket_type, attrs \\ %{}) do TicketType.changeset(ticket_type, attrs) end + + @doc """ + Updates a ticket type's perks. + + ## Examples + + iex> upsert_ticket_type_perks(ticket_type, ["id1", "id2"]) + {:ok, %TicketType{}} + + iex> upsert_ticket_type_perks(ticket_type, ["id1", "id2"]) + {:error, %Ecto.Changeset{}} + + """ + def upsert_ticket_type_perks(%TicketType{} = ticket_type, perk_ids) do + ids = perk_ids || [] + + perks = + Pearl.Tickets.Perk + |> where([p], p.id in ^ids) + |> Repo.all() + + ticket_type + |> Repo.preload(:perks) + |> TicketType.changeset_update_perks(perks) + |> Repo.update() + end + end diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex index fc08298..3863b31 100644 --- a/lib/pearl/tickets.ex +++ b/lib/pearl/tickets.ex @@ -128,4 +128,100 @@ defmodule Pearl.Tickets do def change_ticket(%Ticket{} = ticket, attrs \\ %{}) do Ticket.changeset(ticket, attrs) end + + alias Pearl.Tickets.Perk + + @doc """ + Returns the list of perks. + + ## Examples + + iex> list_perks() + [%Perk{}, ...] + + """ + def list_perks do + Repo.all(Perk) + end + + @doc """ + Gets a single perk. + + Raises `Ecto.NoResultsError` if the Perk does not exist. + + ## Examples + + iex> get_perk!(123) + %Perk{} + + iex> get_perk!(456) + ** (Ecto.NoResultsError) + + """ + def get_perk!(id), do: Repo.get!(Perk, id) + + @doc """ + Creates a perk. + + ## Examples + + iex> create_perk(%{field: value}) + {:ok, %Perk{}} + + iex> create_perk(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_perk(attrs) do + %Perk{} + |> Perk.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a perk. + + ## Examples + + iex> update_perk(perk, %{field: new_value}) + {:ok, %Perk{}} + + iex> update_perk(perk, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_perk(%Perk{} = perk, attrs) do + perk + |> Perk.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a perk. + + ## Examples + + iex> delete_perk(perk) + {:ok, %Perk{}} + + iex> delete_perk(perk) + {:error, %Ecto.Changeset{}} + + """ + def delete_perk(%Perk{} = perk) do + Repo.delete(perk) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking perk changes. + + ## Examples + + iex> change_perk(perk) + %Ecto.Changeset{data: %Perk{}} + + """ + def change_perk(%Perk{} = perk, attrs \\ %{}) do + Perk.changeset(perk, attrs) + end end diff --git a/lib/pearl/tickets/perk.ex b/lib/pearl/tickets/perk.ex new file mode 100644 index 0000000..857896c --- /dev/null +++ b/lib/pearl/tickets/perk.ex @@ -0,0 +1,32 @@ +defmodule Pearl.Tickets.Perk do + @moduledoc """ + Perks for Ticket Types. + """ + + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + @required_fields ~w(name description icon color active)a + + schema "perks" do + field :name, :string + field :description, :string + field :icon, :string + field :color, :string + field :active, :boolean + + many_to_many :ticket_types, Pearl.Tickets.TicketType, + join_through: "ticket_types_perks" + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(perk, attrs) do + perk + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex index 309332b..b0ee860 100644 --- a/lib/pearl/tickets/ticket_type.ex +++ b/lib/pearl/tickets/ticket_type.ex @@ -5,9 +5,10 @@ defmodule Pearl.Tickets.TicketType do use Pearl.Schema alias Pearl.Tickets.Ticket + alias Pearl.Tickets.Perk alias Pearl.DiscountCodes.DiscountCode - @required_fields ~w(name priority description price active)a + @required_fields ~w(name priority price active)a @optional_fields ~w()a @derive {Flop.Schema, sortable: [:priority], filterable: []} @@ -15,12 +16,16 @@ defmodule Pearl.Tickets.TicketType do schema "ticket_types" do field :name, :string field :priority, :integer - field :description, :string - field :price, :integer + field :price, :float field :active, :boolean + field :product_key, :binary_id has_many :tickets, Ticket + many_to_many :perks, Perk, + join_through: "ticket_types_perks", + on_replace: :delete + many_to_many :discount_codes, DiscountCode, join_through: "discount_codes_ticket_types", on_replace: :delete @@ -34,4 +39,10 @@ defmodule Pearl.Tickets.TicketType do |> validate_required(@required_fields) |> foreign_key_constraint(:tickets) end + + def changeset_update_perks(ticket_type, perks) do + ticket_type + |> cast(%{}, @required_fields ++ @optional_fields) + |> put_assoc(:perks, perks) + end end diff --git a/lib/pearl_web/components/core_components.ex b/lib/pearl_web/components/core_components.ex index 16eaa71..e0a2a64 100644 --- a/lib/pearl_web/components/core_components.ex +++ b/lib/pearl_web/components/core_components.ex @@ -295,7 +295,7 @@ defmodule PearlWeb.CoreComponents do name={@name} value="true" checked={@checked} - class={"rounded border-zinc-300 text-accent focus:ring-0 #{@class}"} + class={"rounded border-zinc-300 text-primary focus:ring-0 #{@class}"} {@rest} /> {@label} diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex index 8819a9d..6e7c16d 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -123,26 +123,30 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do end defp save_discount_code(socket, :edit, discount_code_params) do - case DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params) do - {:ok, _discount_code} -> - {:noreply, - socket - |> put_flash(:info, "Discount code updated successfully") - |> push_patch(to: socket.assigns.patch)} - + ticket_type_ids = Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) + + with {:ok, discount_code} <- DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params), + {:ok, _discount_code} <- DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do + {:noreply, + socket + |> put_flash(:info, "Discount code updated successfully") + |> push_patch(to: socket.assigns.patch)} + else {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end end defp save_discount_code(socket, :new, discount_code_params) do - case DiscountCodes.create_discount_code(discount_code_params) do - {:ok, _discount_code} -> - {:noreply, - socket - |> put_flash(:info, "Discount code created successfully") - |> push_patch(to: socket.assigns.patch)} - + ticket_type_ids = Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) + + with {:ok, discount_code} <- DiscountCodes.create_discount_code(discount_code_params), + {:ok, _discount_code} <- DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do + {:noreply, + socket + |> put_flash(:info, "Discount code created successfully") + |> push_patch(to: socket.assigns.patch)} + else {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.ex b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex index 9bfe325..cd77167 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/index.ex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex @@ -1,7 +1,7 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.Index do use PearlWeb, :backoffice_view - import PearlWeb.Components.{Table, TableSearch} + import PearlWeb.Components.Table alias Pearl.DiscountCodes alias Pearl.DiscountCodes.DiscountCode diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex index 7a30000..c2a61ff 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -25,7 +25,7 @@ {discount_code.amount} <:col :let={{_id, discount_code}} sortable field={:active} label="Active"> - {discount_code.active} + <.input type="checkbox" name="active" value="true" checked={discount_code.active} disabled class="text-wine"/> <:col :let={{_id, discount_code}} field={:ticket_types} label="Ticket Types">
diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/index.ex index b2035b9..33bb5f5 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex @@ -3,8 +3,8 @@ defmodule PearlWeb.Backoffice.TicketsLive.Index do import PearlWeb.Components.{Table, TableSearch} - alias Pearl.{Tickets, TicketTypes} - alias Pearl.Tickets.TicketType + alias Pearl.{Tickets, TicketTypes, Perks} + alias Pearl.Tickets.{TicketType, Perk} on_mount {PearlWeb.StaffRoles, index: %{"tickets" => ["edit"]}} @@ -56,6 +56,23 @@ defmodule PearlWeb.Backoffice.TicketsLive.Index do |> assign(:ticket_type, %TicketType{}) end + defp apply_action(socket, :perks, _params) do + socket + |> assign(:page_title, "Listing Perks") + end + + defp apply_action(socket, :perks_new, _params) do + socket + |> assign(:page_title, "New Perk") + |> assign(:perk, %Perk{}) + end + + defp apply_action(socket, :perks_edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Perk") + |> assign(:perk, Perks.get_perk!(id)) + end + def handle_event("delete", %{"id" => id}, socket) do ticket = Tickets.get_ticket!(id) {:ok, _} = Tickets.delete_ticket(ticket) diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex index 330a305..b2a08e5 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -24,7 +24,7 @@ {ticket.user.name} <:col :let={{_id, ticket}} sortable field={:paid} label="Paid"> - {if ticket.paid, do: "Yes", else: "No"} + <.input type="checkbox" name="active" value="true" checked={ticket.paid} disabled class="text-wine"/> <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Ticket Type"> {ticket.ticket_type.name} @@ -82,7 +82,7 @@ title={@page_title} current_user={@current_user} action={@live_action} - patch={~p"/dashboard/tickets"} + patch={~p"/dashboard/tickets/ticket_types"} /> @@ -102,3 +102,36 @@ patch={~p"/dashboard/tickets/ticket_types"} /> + +<.modal + :if={@live_action in [:perks]} + id="perks-modal" + show + on_cancel={JS.patch(~p"/dashboard/tickets/ticket_types/")} +> + <.live_component + module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.Index} + id="list-perks" + title={@page_title} + current_user={@current_user} + action={@live_action} + patch={~p"/dashboard/tickets/ticket_types/perks"} + /> + + +<.modal + :if={@live_action in [:perks_edit, :perks_new]} + id="perks-modal" + show + on_cancel={JS.navigate(~p"/dashboard/tickets/ticket_types/perks")} +> + <.live_component + module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponent} + id={@perk.id || :new} + title={@page_title} + current_user={@current_user} + action={@live_action} + perk={@perk} + patch={~p"/dashboard/tickets/ticket_types/perks/"} + /> + diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex index a1e9756..9442056 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex @@ -2,6 +2,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do use PearlWeb, :live_component alias Pearl.TicketTypes + alias Pearl.Perks import PearlWeb.Components.Forms @@ -25,7 +26,28 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do > <.field field={@form[:name]} type="text" label="Name" required /> <.field field={@form[:description]} type="textarea" label="Description" /> - <.field field={@form[:price]} type="number" label="Price (€)" step="0.01" required /> + <.field field={@form[:price]} type="number" label="Price" required /> + <.field field={@form[:product_key]} type="text" label="Product Key" required /> +
+ +
+ <%= for perk <- @perks do %> + + <% end %> +
+ +
<:actions> <.button phx-disable-with="Saving...">Save Ticket Type @@ -34,11 +56,27 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do """ end + @impl true + def mount(socket) do + {:ok, socket} + end + @impl true def update(%{ticket_type: ticket_type} = assigns, socket) do + perks = Perks.list_perks() + + selected_ids = + case ticket_type.perks do + %Ecto.Association.NotLoaded{} -> [] + perks when is_list(perks) -> Enum.map(perks, & &1.id) + _ -> [] + end + {:ok, socket |> assign(assigns) + |> assign(:perks, perks) + |> assign(:selected_perks_ids, selected_ids) |> assign_new(:form, fn -> to_form(TicketTypes.change_ticket_type(ticket_type)) end)} @@ -47,7 +85,20 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do @impl true def handle_event("validate", %{"ticket_type" => ticket_type_params}, socket) do changeset = TicketTypes.change_ticket_type(socket.assigns.ticket_type, ticket_type_params) - {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + + selected_ids = + case ticket_type_params do + %{"perk_ids" => ids} when is_list(ids) -> + ids |> Enum.reject(&(&1 == "" or is_nil(&1))) + _ -> + [] + end + + {:noreply, + socket + |> assign(form: to_form(changeset, action: :validate)) + |> assign(selected_perks_ids: selected_ids) + } end def handle_event("save", %{"ticket_type" => ticket_type_params}, socket) do @@ -55,30 +106,34 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do end defp save_ticket_type(socket, :ticket_types_edit, ticket_type_params) do - case TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params) do - {:ok, _ticket_type} -> - {:noreply, - socket - |> put_flash(:info, "Ticket type updated successfully") - |> push_patch(to: socket.assigns.patch)} + perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == "")) + with {:ok, ticket_type} <- TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params), + {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do + {:noreply, + socket + |> put_flash(:info, "Ticket type updated successfully") + |> push_patch(to: socket.assigns.patch)} + else {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end end defp save_ticket_type(socket, :ticket_types_new, ticket_type_params) do - case TicketTypes.create_ticket_type( + perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == "")) + + with {:ok, ticket_type} <- TicketTypes.create_ticket_type( ticket_type_params |> Map.put("priority", TicketTypes.get_next_ticket_type_priority()) |> Map.put("active", true) - ) do - {:ok, _ticket_type} -> - {:noreply, - socket - |> put_flash(:info, "Ticket type created successfully") - |> push_patch(to: socket.assigns.patch)} - + ), + {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do + {:noreply, + socket + |> put_flash(:info, "Ticket type created successfully") + |> push_patch(to: socket.assigns.patch)} + else {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex index 229c5d1..500c17d 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex @@ -14,6 +14,9 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index do <.link navigate={~p"/dashboard/tickets/ticket_types/new"}> <.button>New Ticket Type + <.link navigate={~p"/dashboard/tickets/ticket_types/perks"}> + <.button>Perks +
    + <.header> + {@title} + <:subtitle> + {gettext("Perks for the ticket types.")} + + + + <.simple_form + for={@form} + id="perks-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.field field={@form[:name]} type="text" label="Name" required /> + <.field field={@form[:description]} type="textarea" label="Description" /> + <.field field={@form[:icon]} type="text" label="Icon"/> + <.field field={@form[:color]} type="text" label="Color"/> + <:actions> + <.button phx-disable-with="Saving...">Save Perk + + +
+ """ + end + + @impl true + def update(%{perk: perk} = assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_new(:form, fn -> + to_form(Perks.change_perk(perk)) + end)} + end + + @impl true + def handle_event("validate", %{"perk" => perk_params}, socket) do + changeset = Perks.change_perk(socket.assigns.perk, perk_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"perk" => perk_params}, socket) do + save_perk(socket, socket.assigns.action, perk_params) + end + + defp save_perk(socket, :perks_edit, perk_params) do + case Perks.update_perk(socket.assigns.perk, perk_params) do + {:ok, _perk} -> + {:noreply, + socket + |> put_flash(:info, "Perk updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_perk(socket, :perks_new, perk_params) do + case Perks.create_perk( + perk_params + |> Map.put("active", true) + ) do + {:ok, _perk} -> + {:noreply, + socket + |> put_flash(:info, "Perk created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex new file mode 100644 index 0000000..ffcbe7c --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex @@ -0,0 +1,81 @@ +defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.Index do + use PearlWeb, :live_component + + alias Pearl.Perks + import PearlWeb.Components.EnsurePermissions + + @impl true + def render(assigns) do + ~H""" +
+ <.page title={@title}> + <:actions> + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> + <.link navigate={~p"/dashboard/tickets/ticket_types/perks/new"}> + <.button>New Perk + + + +
    +
  • perk.id} + class="even:bg-lightShade/20 dark:even:bg-darkShade/20 py-4 px-4 flex flex-row justify-between" + > +
    + {perk.name} + <%= if not perk.active do %> + + Inactive + + <% end %> +
    +

    + <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> + <.link navigate={~p"/dashboard/tickets/ticket_types/perks/#{perk.id}/edit"}> + <.icon name="hero-pencil" class="w-5 h-4" /> + + <.link + phx-click={JS.push("toggle_archive", value: %{id: perk.id})} + data-confirm="Are you sure?" + phx-target={@myself} + > + <%= if not perk.active do %> + <.icon name="hero-archive-box-arrow-down" class="w-5 h-5" /> + <% else %> + <.icon name="hero-archive-box" class="w-5 h-5" /> + <% end %> + + +

    +
  • +
+ +
+ """ + end + + @impl true + def mount(socket) do + {:ok, + socket + |> stream(:perks, Perks.list_perks())} + end + + @impl true + def handle_event("toggle_archive", %{"id" => id}, socket) do + perk = Perks.get_perk!(id) + + if perk.active do + {:ok, _} = Perks.archive_perk(perk) + else + {:ok, _} = Perks.unarchive_perk(perk) + end + + {:noreply, socket |> stream(:perks, Perks.list_perks())} + end +end diff --git a/lib/pearl_web/router.ex b/lib/pearl_web/router.ex index 0353ebf..af60bb3 100644 --- a/lib/pearl_web/router.ex +++ b/lib/pearl_web/router.ex @@ -236,6 +236,12 @@ defmodule PearlWeb.Router do live "/", Index, :ticket_types live "/new", Index, :ticket_types_new live "/:id/edit", Index, :ticket_types_edit + + scope "/perks" do + live "/", Index, :perks + live "/new", Index, :perks_new + live "/:id/edit", Index, :perks_edit + end end end diff --git a/priv/repo/migrations/20251120141024_add_ticket_types.exs b/priv/repo/migrations/20251120141024_add_ticket_types.exs index be02318..9587b1c 100644 --- a/priv/repo/migrations/20251120141024_add_ticket_types.exs +++ b/priv/repo/migrations/20251120141024_add_ticket_types.exs @@ -6,9 +6,9 @@ defmodule Pearl.Repo.Migrations.AddTicketTypes do add :id, :binary_id, primary_key: true add :priority, :integer add :name, :string - add :description, :string - add :price, :integer + add :price, :float add :active, :boolean + add :product_key, :binary_id timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20251128122529_create_discount_codes.exs b/priv/repo/migrations/20251128122529_create_discount_codes.exs index 0615acf..3f0962e 100644 --- a/priv/repo/migrations/20251128122529_create_discount_codes.exs +++ b/priv/repo/migrations/20251128122529_create_discount_codes.exs @@ -5,8 +5,9 @@ defmodule Pearl.Repo.Migrations.CreateDiscountCodes do create table(:discount_codes, primary_key: false) do add :id, :binary_id, primary_key: true add :code, :string - add :amount, :integer + add :amount, :float add :active, :boolean, default: false, null: false + add :usage_limit, :integer timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs index 723d9a1..3129084 100644 --- a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs +++ b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs @@ -4,9 +4,10 @@ defmodule Pearl.Repo.Migrations.CreateDiscountCodesTicketTypes do def change do create table(:discount_codes_ticket_types, primary_key: false) do add :discount_code_id, - references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false + references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false - add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :delete_all), + add :ticket_type_id, + references(:ticket_types, type: :binary_id, on_delete: :delete_all), null: false end diff --git a/priv/repo/migrations/20251202172259_create_perks.exs b/priv/repo/migrations/20251202172259_create_perks.exs new file mode 100644 index 0000000..ea99c53 --- /dev/null +++ b/priv/repo/migrations/20251202172259_create_perks.exs @@ -0,0 +1,16 @@ +defmodule Pearl.Repo.Migrations.CreatePerks do + use Ecto.Migration + + def change do + create table(:perks, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :string + add :description, :string + add :icon, :string + add :color, :string + add :active, :boolean + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20251202172629_create_jones.exs b/priv/repo/migrations/20251202172629_create_jones.exs new file mode 100644 index 0000000..47101b6 --- /dev/null +++ b/priv/repo/migrations/20251202172629_create_jones.exs @@ -0,0 +1,20 @@ +defmodule Pearl.Repo.Migrations.CreateTicketTypesPerks do + use Ecto.Migration + + def change do + create table(:ticket_types_perks, primary_key: false) do + + add :perk_id, + references(:perks, type: :binary_id, on_delete: :delete_all) + + add :ticket_type_id, + references(:ticket_types, type: :binary_id, on_delete: :delete_all), + null: false + + end + + create index(:ticket_types_perks, [:perk_id]) + create index(:ticket_types_perks, [:ticket_type_id]) + create unique_index(:ticket_types_perks, [:perk_id, :ticket_type_id]) + end +end diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs index 1b0452c..5a39356 100644 --- a/priv/repo/seeds/tickets.exs +++ b/priv/repo/seeds/tickets.exs @@ -2,22 +2,50 @@ defmodule Pearl.Repo.Seeds.Tickets do import Ecto.Query alias Pearl.Accounts.User - alias Pearl.{Repo, Tickets, TicketTypes} - alias Pearl.Tickets.{Ticket, TicketType} + alias Pearl.{Repo, Tickets, TicketTypes, Perks} + alias Pearl.Tickets.{Ticket, TicketType, Perk} + + @perks [ + %{name: "Entry", description: "Entrada nos 4 dias do evento", icon: "hero-ticket", color: "#F9808D", active: true}, + %{name: "Meals", description: "Refeições durante todo o evento", icon: "hero-beaker", color: "#505936", active: true}, + %{name: "Accommodation", description: "Estadia no Pavilhão", icon: "hero-star", color: "#9AB7C1", active: true}, + %{name: "Premium Accommodation", description: "Estadia na Pousada da Juventude", icon: "hero-gift", color: "#D89ED0", active: true}, + ] @ticket_types [ - %{name: "Normal", price: 2500, description: "Normal ticket", active: true, priotity: 0}, - %{name: "FullPass", price: 1500, description: "Premium access", active: true, priority: 1}, - %{name: "FullPass+Hotel", price: 5000, description: "Premium access with hotel", active: true, priority: 2}, - %{name: "Student", price: 3000, description: "Discounted ticket for students", active: true, priority: 3}, - %{name: "Early Bird", price: 2000, description: "Discounted early registration", active: true, priority: 4} + %{name: "Bilhete 1", price: 32, active: true, priority: 0, perks: ["Free Swag"]}, + %{name: "Bilhete 2", price: 33, active: true, priority: 1, perks: ["Free Swag", "Priority Seating"]}, + %{name: "Bilhete 3", price: 38, active: true, priority: 2, perks: ["Free Swag", "Priority Seating", "Early Access"]}, + %{name: "Bilhete 4", price: 45, active: true, priority: 3, perks: ["Early Access", "VIP Lounge", "Free Swag", "Priority Seating"]}, ] def run do + seed_perks() seed_ticket_types() seed_tickets() end + defp seed_perks do + case Repo.all(Perk) do + [] -> + Enum.each(@perks, &insert_perk/1) + Mix.shell().info("Seeded perks successfully.") + + _ -> + Mix.shell().info("Found perks, skipping seeding.") + end + end + + defp insert_perk(attrs) do + case Perks.create_perk(attrs) do + {:ok, _perk} -> + nil + + {:error, _changeset} -> + Mix.shell().error("Failed to insert perk: #{attrs.name}") + end + end + defp seed_ticket_types do case Repo.all(TicketType) do [] -> @@ -30,7 +58,13 @@ defmodule Pearl.Repo.Seeds.Tickets do end defp insert_ticket_type(attrs) do - case TicketTypes.create_ticket_type(attrs) do + {perk_names, ticket_type_attrs} = Map.pop(attrs, :perks, []) + + perks = Repo.all(from p in Perk, where: p.name in ^perk_names) + + ticket_type_attrs = Map.put(ticket_type_attrs, :perks, perks) + + case TicketTypes.create_ticket_type(ticket_type_attrs) do {:ok, _ticket_type} -> nil @@ -49,7 +83,7 @@ defmodule Pearl.Repo.Seeds.Tickets do else ticket_types = Repo.all(TicketType) - empty_ticket_types?(ticket_types, users) + empty_ticket_types?(ticket_types, users) end _ -> @@ -59,21 +93,20 @@ defmodule Pearl.Repo.Seeds.Tickets do defp empty_ticket_types?(ticket_types, users) do if Enum.empty?(ticket_types) do - Mix.shell().error("No ticket types found. Please run ticket types seed first.") - else - users - |> Enum.with_index() - |> Enum.each(fn {user, index} -> - ticket_type = Enum.at(ticket_types, rem(index, length(ticket_types))) - - insert_ticket(%{ - user_id: user.id, - ticket_type_id: ticket_type.id, - paid: rem(index, 3) != 0 - }) - end) - - end + Mix.shell().error("No ticket types found. Please run ticket types seed first.") + else + users + |> Enum.with_index() + |> Enum.each(fn {user, index} -> + ticket_type = Enum.at(ticket_types, rem(index, length(ticket_types))) + + insert_ticket(%{ + user_id: user.id, + ticket_type_id: ticket_type.id, + paid: rem(index, 3) != 0 + }) + end) + end end defp insert_ticket(attrs) do diff --git a/priv/static/images/starts.svg b/priv/static/images/starts.svg index a29bf59..d890a90 100644 --- a/priv/static/images/starts.svg +++ b/priv/static/images/starts.svg @@ -1,8 +1,8 @@ - - + + diff --git a/test/pearl/tickets_test.exs b/test/pearl/tickets_test.exs new file mode 100644 index 0000000..9b7c953 --- /dev/null +++ b/test/pearl/tickets_test.exs @@ -0,0 +1,65 @@ +defmodule Pearl.TicketsTest do + use Pearl.DataCase + + alias Pearl.Tickets + + describe "perks" do + alias Pearl.Tickets.Perk + + import Pearl.TicketsFixtures + + @invalid_attrs %{name: nil, description: nil, color: nil, icon: nil} + + test "list_perks/0 returns all perks" do + perk = perk_fixture() + assert Tickets.list_perks() == [perk] + end + + test "get_perk!/1 returns the perk with given id" do + perk = perk_fixture() + assert Tickets.get_perk!(perk.id) == perk + end + + test "create_perk/1 with valid data creates a perk" do + valid_attrs = %{name: "some name", description: "some description", color: "some color", icon: "some icon"} + + assert {:ok, %Perk{} = perk} = Tickets.create_perk(valid_attrs) + assert perk.name == "some name" + assert perk.description == "some description" + assert perk.color == "some color" + assert perk.icon == "some icon" + end + + test "create_perk/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Tickets.create_perk(@invalid_attrs) + end + + test "update_perk/2 with valid data updates the perk" do + perk = perk_fixture() + update_attrs = %{name: "some updated name", description: "some updated description", color: "some updated color", icon: "some updated icon"} + + assert {:ok, %Perk{} = perk} = Tickets.update_perk(perk, update_attrs) + assert perk.name == "some updated name" + assert perk.description == "some updated description" + assert perk.color == "some updated color" + assert perk.icon == "some updated icon" + end + + test "update_perk/2 with invalid data returns error changeset" do + perk = perk_fixture() + assert {:error, %Ecto.Changeset{}} = Tickets.update_perk(perk, @invalid_attrs) + assert perk == Tickets.get_perk!(perk.id) + end + + test "delete_perk/1 deletes the perk" do + perk = perk_fixture() + assert {:ok, %Perk{}} = Tickets.delete_perk(perk) + assert_raise Ecto.NoResultsError, fn -> Tickets.get_perk!(perk.id) end + end + + test "change_perk/1 returns a perk changeset" do + perk = perk_fixture() + assert %Ecto.Changeset{} = Tickets.change_perk(perk) + end + end +end diff --git a/test/support/fixtures/tickets_fixtures.ex b/test/support/fixtures/tickets_fixtures.ex new file mode 100644 index 0000000..db48231 --- /dev/null +++ b/test/support/fixtures/tickets_fixtures.ex @@ -0,0 +1,23 @@ +defmodule Pearl.TicketsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Pearl.Tickets` context. + """ + + @doc """ + Generate a perk. + """ + def perk_fixture(attrs \\ %{}) do + {:ok, perk} = + attrs + |> Enum.into(%{ + color: "some color", + description: "some description", + icon: "some icon", + name: "some name" + }) + |> Pearl.Tickets.create_perk() + + perk + end +end From 847daa0354436ad333bd7bcb85a6934666f2d63a Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Thu, 4 Dec 2025 19:07:27 +0000 Subject: [PATCH 09/12] feat: perks --- lib/pearl/discount_codes.ex | 1 - lib/pearl/discount_codes/discount_code.ex | 8 +++++- lib/pearl/perks.ex | 3 +-- lib/pearl/ticket_types.ex | 1 - lib/pearl/tickets/perk.ex | 3 +-- lib/pearl/tickets/ticket_type.ex | 4 +-- .../discount_codes_live/form_component.ex | 25 ++++++++++++------- .../discount_codes_live/index.html.heex | 21 ++++++++++------ .../live/backoffice/tickets_live/index.ex | 4 +-- .../backoffice/tickets_live/index.html.heex | 9 ++++++- .../ticket_types_live/form_component.ex | 24 ++++++++++-------- .../perks_live/form_component.ex | 4 +-- ...926_create_discount_codes_ticket_types.exs | 6 ++--- ...51202172629_create_ticket_types_perks.exs} | 8 +++--- priv/repo/seeds/tickets.exs | 4 +-- test/pearl/discount_codes_test.exs | 4 +-- test/pearl/tickets_test.exs | 21 +++++++++++++--- test/support/fixtures/tickets_fixtures.ex | 3 ++- 18 files changed, 96 insertions(+), 57 deletions(-) rename priv/repo/migrations/{20251202172629_create_jones.exs => 20251202172629_create_ticket_types_perks.exs} (70%) diff --git a/lib/pearl/discount_codes.ex b/lib/pearl/discount_codes.ex index 8bab0a3..5228e81 100644 --- a/lib/pearl/discount_codes.ex +++ b/lib/pearl/discount_codes.ex @@ -134,5 +134,4 @@ defmodule Pearl.DiscountCodes do |> DiscountCode.changeset_update_ticket_types(ticket_types) |> Repo.update() end - end diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex index e050983..f701df6 100644 --- a/lib/pearl/discount_codes/discount_code.ex +++ b/lib/pearl/discount_codes/discount_code.ex @@ -1,4 +1,8 @@ defmodule Pearl.DiscountCodes.DiscountCode do + @moduledoc """ + Module for the Discount Code + """ + use Ecto.Schema import Ecto.Changeset @@ -23,7 +27,9 @@ defmodule Pearl.DiscountCodes.DiscountCode do field :active, :boolean, default: false field :usage_limit, :integer - many_to_many :ticket_types, TicketType, join_through: "discount_codes_ticket_types", on_replace: :delete + many_to_many :ticket_types, TicketType, + join_through: "discount_codes_ticket_types", + on_replace: :delete timestamps(type: :utc_datetime) end diff --git a/lib/pearl/perks.ex b/lib/pearl/perks.ex index 779f471..39a50c9 100644 --- a/lib/pearl/perks.ex +++ b/lib/pearl/perks.ex @@ -5,7 +5,7 @@ defmodule Pearl.Perks do use Pearl.Context alias Pearl.Tickets.Perk - def list_perks() do + def list_perks do Repo.all(Perk) end @@ -41,5 +41,4 @@ defmodule Pearl.Perks do |> Perk.changeset(%{active: true}) |> Repo.update() end - end diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex index 22c631b..779af8b 100644 --- a/lib/pearl/ticket_types.ex +++ b/lib/pearl/ticket_types.ex @@ -193,5 +193,4 @@ defmodule Pearl.TicketTypes do |> TicketType.changeset_update_perks(perks) |> Repo.update() end - end diff --git a/lib/pearl/tickets/perk.ex b/lib/pearl/tickets/perk.ex index 857896c..dcac7cb 100644 --- a/lib/pearl/tickets/perk.ex +++ b/lib/pearl/tickets/perk.ex @@ -17,8 +17,7 @@ defmodule Pearl.Tickets.Perk do field :color, :string field :active, :boolean - many_to_many :ticket_types, Pearl.Tickets.TicketType, - join_through: "ticket_types_perks" + many_to_many :ticket_types, Pearl.Tickets.TicketType, join_through: "ticket_types_perks" timestamps(type: :utc_datetime) end diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex index b0ee860..bd06c32 100644 --- a/lib/pearl/tickets/ticket_type.ex +++ b/lib/pearl/tickets/ticket_type.ex @@ -4,9 +4,9 @@ defmodule Pearl.Tickets.TicketType do """ use Pearl.Schema - alias Pearl.Tickets.Ticket - alias Pearl.Tickets.Perk alias Pearl.DiscountCodes.DiscountCode + alias Pearl.Tickets.Perk + alias Pearl.Tickets.Ticket @required_fields ~w(name priority price active)a @optional_fields ~w()a diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex index 6e7c16d..19aab24 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -102,20 +102,22 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do @impl true def handle_event("validate", %{"discount_code" => discount_code_params}, socket) do - changeset = DiscountCodes.change_discount_code(socket.assigns.discount_code, discount_code_params) + changeset = + DiscountCodes.change_discount_code(socket.assigns.discount_code, discount_code_params) selected_ids = case discount_code_params do %{"ticket_type_ids" => ids} when is_list(ids) -> ids |> Enum.reject(&(&1 == "" or is_nil(&1))) + _ -> [] end {:noreply, - socket - |> assign(form: to_form(changeset, action: :validate)) - |> assign(selected_ticket_type_ids: selected_ids)} + socket + |> assign(form: to_form(changeset, action: :validate)) + |> assign(selected_ticket_type_ids: selected_ids)} end def handle_event("save", %{"discount_code" => discount_code_params}, socket) do @@ -123,10 +125,13 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do end defp save_discount_code(socket, :edit, discount_code_params) do - ticket_type_ids = Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) + ticket_type_ids = + Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) - with {:ok, discount_code} <- DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params), - {:ok, _discount_code} <- DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do + with {:ok, discount_code} <- + DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params), + {:ok, _discount_code} <- + DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do {:noreply, socket |> put_flash(:info, "Discount code updated successfully") @@ -138,10 +143,12 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do end defp save_discount_code(socket, :new, discount_code_params) do - ticket_type_ids = Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) + ticket_type_ids = + Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == "")) with {:ok, discount_code} <- DiscountCodes.create_discount_code(discount_code_params), - {:ok, _discount_code} <- DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do + {:ok, _discount_code} <- + DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do {:noreply, socket |> put_flash(:info, "Discount code created successfully") diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex index c2a61ff..7e66573 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -25,20 +25,27 @@ {discount_code.amount} <:col :let={{_id, discount_code}} sortable field={:active} label="Active"> - <.input type="checkbox" name="active" value="true" checked={discount_code.active} disabled class="text-wine"/> + <.input + type="checkbox" + name="active" + value="true" + checked={discount_code.active} + disabled + class="text-wine" + /> <:col :let={{_id, discount_code}} field={:ticket_types} label="Ticket Types">
<%= for ticket_type <- discount_code.ticket_types do %> - {ticket_type.name} + {ticket_type.name} <% end %> <%= if Enum.empty?(discount_code.ticket_types) do %> @@ -80,4 +87,4 @@ discount_code={@discount_code} patch={~p"/dashboard/discount_codes"} /> - \ No newline at end of file + diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/index.ex index 33bb5f5..11a2056 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex @@ -3,8 +3,8 @@ defmodule PearlWeb.Backoffice.TicketsLive.Index do import PearlWeb.Components.{Table, TableSearch} - alias Pearl.{Tickets, TicketTypes, Perks} - alias Pearl.Tickets.{TicketType, Perk} + alias Pearl.{Perks, Tickets, TicketTypes} + alias Pearl.Tickets.{Perk, TicketType} on_mount {PearlWeb.StaffRoles, index: %{"tickets" => ["edit"]}} diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex index b2a08e5..25b5adf 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -24,7 +24,14 @@ {ticket.user.name} <:col :let={{_id, ticket}} sortable field={:paid} label="Paid"> - <.input type="checkbox" name="active" value="true" checked={ticket.paid} disabled class="text-wine"/> + <.input + type="checkbox" + name="active" + value="true" + checked={ticket.paid} + disabled + class="text-wine" + /> <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Ticket Type"> {ticket.ticket_type.name} diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex index 9442056..39420a6 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex @@ -1,8 +1,8 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do use PearlWeb, :live_component - alias Pearl.TicketTypes alias Pearl.Perks + alias Pearl.TicketTypes import PearlWeb.Components.Forms @@ -90,15 +90,15 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do case ticket_type_params do %{"perk_ids" => ids} when is_list(ids) -> ids |> Enum.reject(&(&1 == "" or is_nil(&1))) + _ -> [] end {:noreply, - socket - |> assign(form: to_form(changeset, action: :validate)) - |> assign(selected_perks_ids: selected_ids) - } + socket + |> assign(form: to_form(changeset, action: :validate)) + |> assign(selected_perks_ids: selected_ids)} end def handle_event("save", %{"ticket_type" => ticket_type_params}, socket) do @@ -108,7 +108,8 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do defp save_ticket_type(socket, :ticket_types_edit, ticket_type_params) do perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == "")) - with {:ok, ticket_type} <- TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params), + with {:ok, ticket_type} <- + TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params), {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do {:noreply, socket @@ -123,11 +124,12 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do defp save_ticket_type(socket, :ticket_types_new, ticket_type_params) do perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == "")) - with {:ok, ticket_type} <- TicketTypes.create_ticket_type( - ticket_type_params - |> Map.put("priority", TicketTypes.get_next_ticket_type_priority()) - |> Map.put("active", true) - ), + with {:ok, ticket_type} <- + TicketTypes.create_ticket_type( + ticket_type_params + |> Map.put("priority", TicketTypes.get_next_ticket_type_priority()) + |> Map.put("active", true) + ), {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do {:noreply, socket diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex index 7e18a4b..92b29dc 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex @@ -25,8 +25,8 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponen > <.field field={@form[:name]} type="text" label="Name" required /> <.field field={@form[:description]} type="textarea" label="Description" /> - <.field field={@form[:icon]} type="text" label="Icon"/> - <.field field={@form[:color]} type="text" label="Color"/> + <.field field={@form[:icon]} type="text" label="Icon" /> + <.field field={@form[:color]} type="text" label="Color" /> <:actions> <.button phx-disable-with="Saving...">Save Perk diff --git a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs index 3129084..5a4177e 100644 --- a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs +++ b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs @@ -4,11 +4,11 @@ defmodule Pearl.Repo.Migrations.CreateDiscountCodesTicketTypes do def change do create table(:discount_codes_ticket_types, primary_key: false) do add :discount_code_id, - references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false + references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false add :ticket_type_id, - references(:ticket_types, type: :binary_id, on_delete: :delete_all), - null: false + references(:ticket_types, type: :binary_id, on_delete: :delete_all), + null: false end create index(:discount_codes_ticket_types, [:discount_code_id]) diff --git a/priv/repo/migrations/20251202172629_create_jones.exs b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs similarity index 70% rename from priv/repo/migrations/20251202172629_create_jones.exs rename to priv/repo/migrations/20251202172629_create_ticket_types_perks.exs index 47101b6..7ee2dfa 100644 --- a/priv/repo/migrations/20251202172629_create_jones.exs +++ b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs @@ -3,14 +3,12 @@ defmodule Pearl.Repo.Migrations.CreateTicketTypesPerks do def change do create table(:ticket_types_perks, primary_key: false) do - add :perk_id, - references(:perks, type: :binary_id, on_delete: :delete_all) + references(:perks, type: :binary_id, on_delete: :delete_all) add :ticket_type_id, - references(:ticket_types, type: :binary_id, on_delete: :delete_all), - null: false - + references(:ticket_types, type: :binary_id, on_delete: :delete_all), + null: false end create index(:ticket_types_perks, [:perk_id]) diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs index 5a39356..b1871ac 100644 --- a/priv/repo/seeds/tickets.exs +++ b/priv/repo/seeds/tickets.exs @@ -2,8 +2,8 @@ defmodule Pearl.Repo.Seeds.Tickets do import Ecto.Query alias Pearl.Accounts.User - alias Pearl.{Repo, Tickets, TicketTypes, Perks} - alias Pearl.Tickets.{Ticket, TicketType, Perk} + alias Pearl.{Perks, Repo, Tickets, TicketTypes} + alias Pearl.Tickets.{Perk, Ticket, TicketType} @perks [ %{name: "Entry", description: "Entrada nos 4 dias do evento", icon: "hero-ticket", color: "#F9808D", active: true}, diff --git a/test/pearl/discount_codes_test.exs b/test/pearl/discount_codes_test.exs index b856803..7377ed9 100644 --- a/test/pearl/discount_codes_test.exs +++ b/test/pearl/discount_codes_test.exs @@ -11,8 +11,8 @@ defmodule Pearl.DiscountCodesTest do @invalid_attrs %{active: nil, code: nil, amount: nil} test "list_discount_codes/0 returns all discount_codes" do - discount_code = discount_code_fixture() - assert DiscountCodes.list_discount_codes() == [discount_code] + _discount_code = discount_code_fixture() + assert {:ok, {[_discount_code], _meta}} = DiscountCodes.list_discount_codes() end test "get_discount_code!/1 returns the discount_code with given id" do diff --git a/test/pearl/tickets_test.exs b/test/pearl/tickets_test.exs index 9b7c953..49b130c 100644 --- a/test/pearl/tickets_test.exs +++ b/test/pearl/tickets_test.exs @@ -8,7 +8,7 @@ defmodule Pearl.TicketsTest do import Pearl.TicketsFixtures - @invalid_attrs %{name: nil, description: nil, color: nil, icon: nil} + @invalid_attrs %{name: nil, description: nil, color: nil, icon: nil, active: nil} test "list_perks/0 returns all perks" do perk = perk_fixture() @@ -21,13 +21,20 @@ defmodule Pearl.TicketsTest do end test "create_perk/1 with valid data creates a perk" do - valid_attrs = %{name: "some name", description: "some description", color: "some color", icon: "some icon"} + valid_attrs = %{ + name: "some name", + description: "some description", + color: "some color", + icon: "some icon", + active: true + } assert {:ok, %Perk{} = perk} = Tickets.create_perk(valid_attrs) assert perk.name == "some name" assert perk.description == "some description" assert perk.color == "some color" assert perk.icon == "some icon" + assert perk.active == true end test "create_perk/1 with invalid data returns error changeset" do @@ -36,13 +43,21 @@ defmodule Pearl.TicketsTest do test "update_perk/2 with valid data updates the perk" do perk = perk_fixture() - update_attrs = %{name: "some updated name", description: "some updated description", color: "some updated color", icon: "some updated icon"} + + update_attrs = %{ + name: "some updated name", + description: "some updated description", + color: "some updated color", + icon: "some updated icon", + active: true + } assert {:ok, %Perk{} = perk} = Tickets.update_perk(perk, update_attrs) assert perk.name == "some updated name" assert perk.description == "some updated description" assert perk.color == "some updated color" assert perk.icon == "some updated icon" + assert perk.active == true end test "update_perk/2 with invalid data returns error changeset" do diff --git a/test/support/fixtures/tickets_fixtures.ex b/test/support/fixtures/tickets_fixtures.ex index db48231..85c8b60 100644 --- a/test/support/fixtures/tickets_fixtures.ex +++ b/test/support/fixtures/tickets_fixtures.ex @@ -14,7 +14,8 @@ defmodule Pearl.TicketsFixtures do color: "some color", description: "some description", icon: "some icon", - name: "some name" + name: "some name", + active: true }) |> Pearl.Tickets.create_perk() From 203f84fefc581e4ab573f9a4d458dc826ee621fe Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Thu, 4 Dec 2025 19:26:39 +0000 Subject: [PATCH 10/12] feat: add usage_limit to backoffice view --- lib/pearl/discount_codes/discount_code.ex | 2 +- .../discount_codes_live/form_component.ex | 7 +++++++ .../discount_codes_live/index.html.heex | 3 +++ priv/repo/seeds/discount_codes.exs | 18 ++++++++++++------ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex index f701df6..03b0e17 100644 --- a/lib/pearl/discount_codes/discount_code.ex +++ b/lib/pearl/discount_codes/discount_code.ex @@ -15,7 +15,7 @@ defmodule Pearl.DiscountCodes.DiscountCode do default_limit: 25 } - @required_fields ~w(code amount active)a + @required_fields ~w(code amount active usage_limit)a @optional_fields ~w()a @primary_key {:id, :binary_id, autogenerate: true} diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex index 19aab24..a51af7b 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -39,6 +39,13 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do required /> + <.field + field={@form[:usage_limit]} + type="number" + label="Usage Limit" + required + /> + <.field field={@form[:active]} type="checkbox" diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex index 7e66573..a903907 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -34,6 +34,9 @@ class="text-wine" /> + <:col :let={{_id, discount_code}} field={:usage_limit} label="Usage Limit"> + {discount_code.usage_limit} + <:col :let={{_id, discount_code}} field={:ticket_types} label="Ticket Types">
<%= for ticket_type <- discount_code.ticket_types do %> diff --git a/priv/repo/seeds/discount_codes.exs b/priv/repo/seeds/discount_codes.exs index d6f88bf..248b1c7 100644 --- a/priv/repo/seeds/discount_codes.exs +++ b/priv/repo/seeds/discount_codes.exs @@ -18,37 +18,43 @@ else code: "EARLYBIRD2025", amount: 10, active: true, - ticket_type_ids: [normal, early_bird] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + ticket_type_ids: [normal, early_bird] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 50 }, %{ code: "STUDENT50", amount: 20, active: true, - ticket_type_ids: [student] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + ticket_type_ids: [student] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 20 }, %{ code: "FULLPASS20", amount: 100, active: true, - ticket_type_ids: [fullpass, fullpass_hotel] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + ticket_type_ids: [fullpass, fullpass_hotel] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 12 }, %{ code: "ALLACCESS", amount: 100, active: true, - ticket_type_ids: ticket_types |> Enum.map(& &1.id) + ticket_type_ids: ticket_types |> Enum.map(& &1.id), + usage_limit: 3 }, %{ code: "SPONSOR25", amount: 100, active: true, - ticket_type_ids: [normal, fullpass] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + ticket_type_ids: [normal, fullpass] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 1 }, %{ code: "EXPIRED2024", amount: 33, active: false, - ticket_type_ids: [normal] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id) + ticket_type_ids: [normal] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 10 } ] From bb8e200489d32d32f18bb2e1a6d3df735aa2a345 Mon Sep 17 00:00:00 2001 From: Enrico Prazeres Date: Thu, 4 Dec 2025 21:34:17 +0000 Subject: [PATCH 11/12] fix: ticket seeds --- priv/repo/seeds/tickets.exs | 25 +++++++++++-------- test/pearl/discount_codes_test.exs | 6 ++--- .../fixtures/discount_codes_fixtures.ex | 3 ++- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs index b1871ac..c3fb138 100644 --- a/priv/repo/seeds/tickets.exs +++ b/priv/repo/seeds/tickets.exs @@ -9,14 +9,14 @@ defmodule Pearl.Repo.Seeds.Tickets do %{name: "Entry", description: "Entrada nos 4 dias do evento", icon: "hero-ticket", color: "#F9808D", active: true}, %{name: "Meals", description: "Refeições durante todo o evento", icon: "hero-beaker", color: "#505936", active: true}, %{name: "Accommodation", description: "Estadia no Pavilhão", icon: "hero-star", color: "#9AB7C1", active: true}, - %{name: "Premium Accommodation", description: "Estadia na Pousada da Juventude", icon: "hero-gift", color: "#D89ED0", active: true}, + %{name: "Premium Accommodation", description: "Estadia na Pousada da Juventude", icon: "hero-gift", color: "#D89ED0", active: true} ] @ticket_types [ - %{name: "Bilhete 1", price: 32, active: true, priority: 0, perks: ["Free Swag"]}, - %{name: "Bilhete 2", price: 33, active: true, priority: 1, perks: ["Free Swag", "Priority Seating"]}, - %{name: "Bilhete 3", price: 38, active: true, priority: 2, perks: ["Free Swag", "Priority Seating", "Early Access"]}, - %{name: "Bilhete 4", price: 45, active: true, priority: 3, perks: ["Early Access", "VIP Lounge", "Free Swag", "Priority Seating"]}, + %{name: "Bilhete 1", description: "A nice ticket", price: 32, active: true, product_key: "XxXxX", priority: 0, perks: ["Entry"]}, + %{name: "Bilhete 2", description: "A much nicer ticket", price: 33, active: true, product_key: "XxXxX", priority: 1, perks: ["Entry", "Meals"]}, + %{name: "Bilhete 3", description: "An awesome ticket", price: 38, active: true, product_key: "XxXxX", priority: 2, perks: ["Entry", "Meals", "Accommodation"]}, + %{name: "Bilhete 4", description: "Absolutely magnificent ticket", price: 45, product_key: "XxXxX", active: true, priority: 3, perks: ["Entry", "Meals", "Premium Accommodation"]} ] def run do @@ -60,13 +60,18 @@ defmodule Pearl.Repo.Seeds.Tickets do defp insert_ticket_type(attrs) do {perk_names, ticket_type_attrs} = Map.pop(attrs, :perks, []) - perks = Repo.all(from p in Perk, where: p.name in ^perk_names) + case TicketTypes.create_ticket_type(ticket_type_attrs) do + {:ok, ticket_type} -> + perk_ids = + Repo.all(from p in Perk, where: p.name in ^perk_names, select: p.id) - ticket_type_attrs = Map.put(ticket_type_attrs, :perks, perks) + case TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do + {:ok, _ticket_type} -> + nil - case TicketTypes.create_ticket_type(ticket_type_attrs) do - {:ok, _ticket_type} -> - nil + {:error, _changeset} -> + Mix.shell().error("Failed to associate perks for ticket type: #{attrs.name}") + end {:error, _changeset} -> Mix.shell().error("Failed to insert ticket type: #{attrs.name}") diff --git a/test/pearl/discount_codes_test.exs b/test/pearl/discount_codes_test.exs index 7377ed9..5784df2 100644 --- a/test/pearl/discount_codes_test.exs +++ b/test/pearl/discount_codes_test.exs @@ -8,7 +8,7 @@ defmodule Pearl.DiscountCodesTest do import Pearl.DiscountCodesFixtures - @invalid_attrs %{active: nil, code: nil, amount: nil} + @invalid_attrs %{active: nil, code: nil, amount: nil, usage_limit: nil} test "list_discount_codes/0 returns all discount_codes" do _discount_code = discount_code_fixture() @@ -21,7 +21,7 @@ defmodule Pearl.DiscountCodesTest do end test "create_discount_code/1 with valid data creates a discount_code" do - valid_attrs = %{active: true, code: "some code", amount: 42} + valid_attrs = %{active: true, code: "some code", amount: 42, usage_limit: 1} assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.create_discount_code(valid_attrs) @@ -37,7 +37,7 @@ defmodule Pearl.DiscountCodesTest do test "update_discount_code/2 with valid data updates the discount_code" do discount_code = discount_code_fixture() - update_attrs = %{active: false, code: "some updated code", amount: 43} + update_attrs = %{active: false, code: "some updated code", amount: 43, usage_limit: 2} assert {:ok, %DiscountCode{} = discount_code} = DiscountCodes.update_discount_code(discount_code, update_attrs) diff --git a/test/support/fixtures/discount_codes_fixtures.ex b/test/support/fixtures/discount_codes_fixtures.ex index 0734205..f3d14ae 100644 --- a/test/support/fixtures/discount_codes_fixtures.ex +++ b/test/support/fixtures/discount_codes_fixtures.ex @@ -13,7 +13,8 @@ defmodule Pearl.DiscountCodesFixtures do |> Enum.into(%{ active: true, amount: 42, - code: "some code" + code: "some code", + usage_limit: 100 }) |> Pearl.DiscountCodes.create_discount_code() From bdf4cbbe16f9068383e5a16db6ed576d6592c4a6 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Tue, 23 Dec 2025 18:44:06 +0000 Subject: [PATCH 12/12] fix: button components --- .../live/backoffice/discount_codes_live/form_component.ex | 2 +- .../live/backoffice/discount_codes_live/index.html.heex | 4 ++-- lib/pearl_web/live/backoffice/tickets_live/form_component.ex | 2 +- lib/pearl_web/live/backoffice/tickets_live/index.html.heex | 4 ++-- .../tickets_live/ticket_types_live/form_component.ex | 2 +- .../live/backoffice/tickets_live/ticket_types_live/index.ex | 4 ++-- .../ticket_types_live/perks_live/form_component.ex | 2 +- .../tickets_live/ticket_types_live/perks_live/index.ex | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex index a51af7b..429274a 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -74,7 +74,7 @@ defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do
<:actions> - <.button phx-disable-with="Saving...">Save Discount Code + <.backoffice_button phx-disable-with="Saving...">Save Discount Code
diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex index a903907..18e94f9 100644 --- a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -3,9 +3,9 @@
<.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}> <.link patch={~p"/dashboard/discount_codes/new"}> - <.button> + <.backoffice_button> New Discount - +
diff --git a/lib/pearl_web/live/backoffice/tickets_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex index 419b4da..95f3efd 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex @@ -44,7 +44,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.FormComponent do
<:actions> - <.button phx-disable-with="Saving...">Save Ticket + <.backoffice_button phx-disable-with="Saving...">Save Ticket diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex index 25b5adf..75a3d1a 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -10,9 +10,9 @@ /> <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> <.link patch={~p"/dashboard/tickets/ticket_types"}> - <.button> + <.backoffice_button> <.icon name="hero-inbox-stack" class="w-5 h-5" /> - + diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex index 39420a6..6ac62ac 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex @@ -49,7 +49,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do <:actions> - <.button phx-disable-with="Saving...">Save Ticket Type + <.backoffice_button phx-disable-with="Saving...">Save Ticket Type diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex index 500c17d..a662fa3 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex @@ -12,10 +12,10 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index do <:actions> <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> <.link navigate={~p"/dashboard/tickets/ticket_types/new"}> - <.button>New Ticket Type + <.backoffice_button>New Ticket Type <.link navigate={~p"/dashboard/tickets/ticket_types/perks"}> - <.button>Perks + <.backoffice_button>Perks diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex index 92b29dc..65842c1 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex @@ -28,7 +28,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponen <.field field={@form[:icon]} type="text" label="Icon" /> <.field field={@form[:color]} type="text" label="Color" /> <:actions> - <.button phx-disable-with="Saving...">Save Perk + <.backoffice_button phx-disable-with="Saving...">Save Perk diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex index ffcbe7c..1e3697b 100644 --- a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex @@ -12,7 +12,7 @@ defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.Index do <:actions> <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}> <.link navigate={~p"/dashboard/tickets/ticket_types/perks/new"}> - <.button>New Perk + <.backoffice_button>New Perk