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/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/ticket_types.ex b/lib/pearl/ticket_types.ex
new file mode 100644
index 0000000..62c357c
--- /dev/null
+++ b/lib/pearl/ticket_types.ex
@@ -0,0 +1,168 @@
+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
+ Repo.get!(TicketType, 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 """
+ 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
+end
diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex
new file mode 100644
index 0000000..fc08298
--- /dev/null
+++ b/lib/pearl/tickets.ex
@@ -0,0 +1,131 @@
+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
+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..9eae3ae
--- /dev/null
+++ b/lib/pearl/tickets/ticket_type.ex
@@ -0,0 +1,32 @@
+defmodule Pearl.Tickets.TicketType do
+ @moduledoc """
+ Ticket types for Tickets.
+ """
+ use Pearl.Schema
+
+ alias Pearl.Tickets.Ticket
+
+ @required_fields ~w(name priority description price active)a
+ @optional_fields ~w()a
+
+ @derive {Flop.Schema, sortable: [:priority], filterable: []}
+
+ schema "ticket_types" do
+ field :name, :string
+ field :priority, :integer
+ field :description, :string
+ field :price, :integer
+ field :active, :boolean
+
+ has_many :tickets, Ticket
+
+ 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
+end
diff --git a/lib/pearl_web/config.ex b/lib/pearl_web/config.ex
index 92d343b..a2196fb 100644
--- a/lib/pearl_web/config.ex
+++ b/lib/pearl_web/config.ex
@@ -158,6 +158,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..42e4f16
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex
@@ -0,0 +1,107 @@
+<.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..229c5d1
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex
@@ -0,0 +1,95 @@
+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
new file mode 100644
index 0000000..be02318
--- /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 :description, :string
+ add :price, :integer
+ add :active, :boolean
+
+ 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/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..1b0452c
--- /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, 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()