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 + + + + + +
+ """ + 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()