diff --git a/lib/pearl/accounts/roles/permissions.ex b/lib/pearl/accounts/roles/permissions.ex index b5d5240..b920541 100644 --- a/lib/pearl/accounts/roles/permissions.ex +++ b/lib/pearl/accounts/roles/permissions.ex @@ -10,6 +10,8 @@ defmodule Pearl.Accounts.Roles.Permissions do "staffs" => ["show", "edit", "roles_edit"], "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/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/discount_codes.ex b/lib/pearl/discount_codes.ex new file mode 100644 index 0000000..5228e81 --- /dev/null +++ b/lib/pearl/discount_codes.ex @@ -0,0 +1,137 @@ +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(params \\ %{}) do + DiscountCode + |> preload(:ticket_types) + |> Flop.validate_and_run(params, for: 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 + DiscountCode + |> Repo.get!(id) + |> Repo.preload(:ticket_types) + end + + @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{} + |> Repo.preload(:ticket_types) + |> 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 + |> Repo.preload(:ticket_types) + |> 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 + + @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 new file mode 100644 index 0000000..03b0e17 --- /dev/null +++ b/lib/pearl/discount_codes/discount_code.ex @@ -0,0 +1,50 @@ +defmodule Pearl.DiscountCodes.DiscountCode do + @moduledoc """ + Module for the Discount Code + """ + + 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 usage_limit)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, :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 + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(discount_code, attrs) do + discount_code + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end + + @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 +end diff --git a/lib/pearl/perks.ex b/lib/pearl/perks.ex new file mode 100644 index 0000000..39a50c9 --- /dev/null +++ b/lib/pearl/perks.ex @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..779af8b --- /dev/null +++ b/lib/pearl/ticket_types.ex @@ -0,0 +1,196 @@ +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 + TicketType + |> order_by(:priority) + |> 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. + + 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) + |> Repo.preload(:perks) + 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 """ + 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. + + ## 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 + + @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 new file mode 100644 index 0000000..3863b31 --- /dev/null +++ b/lib/pearl/tickets.ex @@ -0,0 +1,227 @@ +defmodule Pearl.Tickets do + @moduledoc """ + The Tickets context. + """ + 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 + Ticket + |> Repo.all() + end + + def list_tickets(opts) when is_list(opts) do + Ticket + |> apply_filters(opts) + |> 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. + + 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, :ticket_type]) + |> 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 + + 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..dcac7cb --- /dev/null +++ b/lib/pearl/tickets/perk.ex @@ -0,0 +1,31 @@ +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.ex b/lib/pearl/tickets/ticket.ex new file mode 100644 index 0000000..52ea106 --- /dev/null +++ b/lib/pearl/tickets/ticket.ex @@ -0,0 +1,53 @@ +defmodule Pearl.Tickets.Ticket do + @moduledoc """ + Tickets to access the event. + """ + + 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, on_replace: :delete + + timestamps(type: :utc_datetime) + end + + def changeset(ticket, attrs) do + ticket + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id) + |> 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 new file mode 100644 index 0000000..bd06c32 --- /dev/null +++ b/lib/pearl/tickets/ticket_type.ex @@ -0,0 +1,48 @@ +defmodule Pearl.Tickets.TicketType do + @moduledoc """ + Ticket types for Tickets. + """ + use Pearl.Schema + + alias Pearl.DiscountCodes.DiscountCode + alias Pearl.Tickets.Perk + alias Pearl.Tickets.Ticket + + @required_fields ~w(name priority price active)a + @optional_fields ~w()a + + @derive {Flop.Schema, sortable: [:priority], filterable: []} + + schema "ticket_types" do + field :name, :string + field :priority, :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 + + timestamps(type: :utc_datetime) + end + + def changeset(ticket_type, attrs) do + ticket_type + |> cast(attrs, @required_fields ++ @optional_fields) + |> 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/config.ex b/lib/pearl_web/config.ex index e2b9699..a01c883 100644 --- a/lib/pearl_web/config.ex +++ b/lib/pearl_web/config.ex @@ -153,6 +153,20 @@ defmodule PearlWeb.Config do url: "/dashboard/companies", scope: %{"companies" => ["edit"]} }, + %{ + key: :tickets, + title: "Tickets", + icon: "hero-ticket", + 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..429274a --- /dev/null +++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex @@ -0,0 +1,168 @@ +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[:usage_limit]} + type="number" + label="Usage Limit" + required + /> + + <.field + field={@form[:active]} + type="checkbox" + label="Active" + /> + +
+ +
+ <%= for ticket_type <- @ticket_types do %> + + <% end %> +
+ +
+ + <:actions> + <.backoffice_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 + 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 + 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 + 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..cd77167 --- /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 + + 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..18e94f9 --- /dev/null +++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex @@ -0,0 +1,93 @@ +<.page title="Discount Codes"> + <:actions> +
+ <.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}> + <.link patch={~p"/dashboard/discount_codes/new"}> + <.backoffice_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"> + <.input + type="checkbox" + name="active" + value="true" + checked={discount_code.active} + disabled + 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 %> + + {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"} + /> + 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..95f3efd --- /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> + <.backoffice_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..11a2056 --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex @@ -0,0 +1,82 @@ +defmodule PearlWeb.Backoffice.TicketsLive.Index do + use PearlWeb, :backoffice_view + + import PearlWeb.Components.{Table, TableSearch} + + alias Pearl.{Perks, Tickets, TicketTypes} + alias Pearl.Tickets.{Perk, 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(:current_page, :tickets) + |> 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 + + 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) + + {: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..75a3d1a --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex @@ -0,0 +1,144 @@ +<.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"}> + <.backoffice_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"> + <.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} + <%= if not ticket.ticket_type.active do %> + + Inactive + + <% end %> + + <: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/ticket_types"} + /> + + +<.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"} + /> + + +<.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 new file mode 100644 index 0000000..6ac62ac --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex @@ -0,0 +1,143 @@ +defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do + use PearlWeb, :live_component + + alias Pearl.Perks + 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" required /> + <.field field={@form[:product_key]} type="text" label="Product Key" required /> +
+ +
+ <%= for perk <- @perks do %> + + <% end %> +
+ +
+ <:actions> + <.backoffice_button phx-disable-with="Saving...">Save Ticket Type + + +
+ """ + 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)} + 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) + + 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 + save_ticket_type(socket, socket.assigns.action, ticket_type_params) + end + + 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), + {: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 + 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) + ), + {: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 + 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..a662fa3 --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex @@ -0,0 +1,98 @@ +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"}> + <.backoffice_button>New Ticket Type + + <.link navigate={~p"/dashboard/tickets/ticket_types/perks"}> + <.backoffice_button>Perks + + + + + +
+ """ + 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/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 new file mode 100644 index 0000000..65842c1 --- /dev/null +++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex @@ -0,0 +1,86 @@ +defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponent do + use PearlWeb, :live_component + + alias Pearl.Perks + + import PearlWeb.Components.Forms + + @impl true + def render(assigns) do + ~H""" +
+ <.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> + <.backoffice_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..1e3697b --- /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"}> + <.backoffice_button>New Perk + + + + + +
+ """ + 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 7c82390..af60bb3 100644 --- a/lib/pearl_web/router.ex +++ b/lib/pearl_web/router.ex @@ -228,6 +228,29 @@ 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 + + scope "/perks" do + live "/", Index, :perks + live "/new", Index, :perks_new + live "/:id/edit", Index, :perks_edit + end + 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/20251120141024_add_ticket_types.exs b/priv/repo/migrations/20251120141024_add_ticket_types.exs new file mode 100644 index 0000000..9587b1c --- /dev/null +++ b/priv/repo/migrations/20251120141024_add_ticket_types.exs @@ -0,0 +1,16 @@ +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 :priority, :integer + add :name, :string + add :price, :float + add :active, :boolean + add :product_key, :binary_id + + 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..1ad61e1 --- /dev/null +++ b/priv/repo/migrations/20251120141058_add_tickets.exs @@ -0,0 +1,19 @@ +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 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..3f0962e --- /dev/null +++ b/priv/repo/migrations/20251128122529_create_discount_codes.exs @@ -0,0 +1,15 @@ +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, :float + add :active, :boolean, default: false, null: false + add :usage_limit, :integer + + timestamps(type: :utc_datetime) + end + end +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 new file mode 100644 index 0000000..5a4177e --- /dev/null +++ b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs @@ -0,0 +1,18 @@ +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/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_ticket_types_perks.exs b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs new file mode 100644 index 0000000..7ee2dfa --- /dev/null +++ b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs @@ -0,0 +1,18 @@ +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.exs b/priv/repo/seeds.exs index cf725c6..6309f3d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,7 +21,9 @@ defmodule Pearl.Repo.Seeds do "companies.exs", "activities.exs", "slots.exs", - "teams.exs" + "teams.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..248b1c7 --- /dev/null +++ b/priv/repo/seeds/discount_codes.exs @@ -0,0 +1,77 @@ +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), + usage_limit: 50 + }, + %{ + code: "STUDENT50", + amount: 20, + active: true, + 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), + usage_limit: 12 + }, + %{ + code: "ALLACCESS", + amount: 100, + active: true, + 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), + usage_limit: 1 + }, + %{ + code: "EXPIRED2024", + amount: 33, + active: false, + ticket_type_ids: [normal] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id), + usage_limit: 10 + } + ] + + 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/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs new file mode 100644 index 0000000..c3fb138 --- /dev/null +++ b/priv/repo/seeds/tickets.exs @@ -0,0 +1,128 @@ +defmodule Pearl.Repo.Seeds.Tickets do + import Ecto.Query + + alias Pearl.Accounts.User + 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}, + %{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: "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 + 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 + [] -> + 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 + {perk_names, ticket_type_attrs} = Map.pop(attrs, :perks, []) + + 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) + + case TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) 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}") + 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, users) + end + + _ -> + Mix.shell().info("Found tickets, skipping seeding.") + end + end + + 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 + 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() 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/discount_codes_test.exs b/test/pearl/discount_codes_test.exs new file mode 100644 index 0000000..5784df2 --- /dev/null +++ b/test/pearl/discount_codes_test.exs @@ -0,0 +1,73 @@ +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, usage_limit: nil} + + test "list_discount_codes/0 returns all discount_codes" do + _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 + 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, usage_limit: 1} + + 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, usage_limit: 2} + + 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/pearl/tickets_test.exs b/test/pearl/tickets_test.exs new file mode 100644 index 0000000..49b130c --- /dev/null +++ b/test/pearl/tickets_test.exs @@ -0,0 +1,80 @@ +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, active: 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", + 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 + 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", + 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 + 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/discount_codes_fixtures.ex b/test/support/fixtures/discount_codes_fixtures.ex new file mode 100644 index 0000000..f3d14ae --- /dev/null +++ b/test/support/fixtures/discount_codes_fixtures.ex @@ -0,0 +1,23 @@ +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", + usage_limit: 100 + }) + |> Pearl.DiscountCodes.create_discount_code() + + discount_code + end +end diff --git a/test/support/fixtures/tickets_fixtures.ex b/test/support/fixtures/tickets_fixtures.ex new file mode 100644 index 0000000..85c8b60 --- /dev/null +++ b/test/support/fixtures/tickets_fixtures.ex @@ -0,0 +1,24 @@ +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", + active: true + }) + |> Pearl.Tickets.create_perk() + + perk + end +end