From 8708f81a9aeb24dff76781d2c9b0b1e3b16782ea Mon Sep 17 00:00:00 2001 From: feliciofilipe Date: Sat, 18 Jun 2022 19:43:33 +0100 Subject: [PATCH] Add inventory --- lib/parzival/accounts/user.ex | 2 + lib/parzival/gamification.ex | 19 +- lib/parzival/store.ex | 264 +++++++++++++++++- lib/parzival/store/boost.ex | 53 ++++ lib/parzival/store/item.ex | 31 ++ lib/parzival/store/order.ex | 1 - lib/parzival/store/product.ex | 2 +- lib/parzival/uploaders/boost_image.ex | 45 +++ lib/parzival_web/components/boost.ex | 42 +++ .../gamification/leaderboard_live/index.ex | 2 +- .../live/app/jobs/offer_live/index.html.heex | 6 +- .../live/app/store/boost_live/index.ex | 34 +++ .../live/app/store/boost_live/index.html.heex | 49 ++++ .../live/app/store/boost_live/show.ex | 85 ++++++ .../live/app/store/boost_live/show.html.heex | 68 +++++ .../app/store/product_live/index.html.heex | 71 +++-- .../live/app/store/product_live/show.ex | 20 +- .../live/app/vault/order_live/index.html.heex | 13 - .../store/item_live/form_component.ex | 55 ++++ .../store/item_live/form_component.html.heex | 9 + lib/parzival_web/live/hooks.ex | 6 +- lib/parzival_web/router.ex | 3 + .../templates/layout/live.html.heex | 30 +- lib/parzival_web/views/view_utils.ex | 7 + priv/fake/recruiters.txt | 1 + .../20220619010759_create_boosts.exs | 19 ++ .../20220620001713_create_items.exs | 19 ++ priv/repo/seeds/store.exs | 69 +++++ .../defaults/store/boost_image_original.png | Bin 0 -> 461150 bytes .../defaults/store/product_image_original.png | Bin 80186 -> 461150 bytes test/parzival/store_test.exs | 106 +++++++ test/parzival_web/live/boost_live_test.exs | 105 +++++++ test/parzival_web/live/item_live_test.exs | 105 +++++++ test/support/fixtures/store_fixtures.ex | 26 ++ 34 files changed, 1295 insertions(+), 72 deletions(-) create mode 100644 lib/parzival/store/boost.ex create mode 100644 lib/parzival/store/item.ex create mode 100644 lib/parzival/uploaders/boost_image.ex create mode 100644 lib/parzival_web/components/boost.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/index.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/index.html.heex create mode 100644 lib/parzival_web/live/app/store/boost_live/show.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/show.html.heex create mode 100644 lib/parzival_web/live/backoffice/store/item_live/form_component.ex create mode 100644 lib/parzival_web/live/backoffice/store/item_live/form_component.html.heex create mode 100644 priv/repo/migrations/20220619010759_create_boosts.exs create mode 100644 priv/repo/migrations/20220620001713_create_items.exs create mode 100644 priv/static/images/defaults/store/boost_image_original.png create mode 100644 test/parzival_web/live/boost_live_test.exs create mode 100644 test/parzival_web/live/item_live_test.exs diff --git a/lib/parzival/accounts/user.ex b/lib/parzival/accounts/user.ex index 30403bd0..d381905d 100644 --- a/lib/parzival/accounts/user.ex +++ b/lib/parzival/accounts/user.ex @@ -8,6 +8,7 @@ defmodule Parzival.Accounts.User do alias Parzival.Companies.Company alias Parzival.Gamification.Curriculum alias Parzival.Gamification.Mission + alias Parzival.Store.Item alias Parzival.Store.Order alias Parzival.Uploaders @@ -62,6 +63,7 @@ defmodule Parzival.Accounts.User do has_one :curriculum, Curriculum has_many :orders, Order + has_many :inventory, Item many_to_many :missions, Mission, join_through: "missions_users" diff --git a/lib/parzival/gamification.ex b/lib/parzival/gamification.ex index 10d87499..9d2f2919 100644 --- a/lib/parzival/gamification.ex +++ b/lib/parzival/gamification.ex @@ -7,6 +7,7 @@ defmodule Parzival.Gamification do alias Ecto.Multi + alias Parzival.Accounts alias Parzival.Accounts.User alias Parzival.Gamification.Curriculum alias Parzival.Gamification.Curriculum.Education @@ -786,7 +787,7 @@ defmodule Parzival.Gamification do user |> User.task_completion_changeset(%{ - balance: user.balance + mission.tokens, + balance: user.balance + mission.tokens * get_tokens_multiplier(user), exp: user.exp + mission.exp }) |> repo.update() @@ -807,6 +808,22 @@ defmodule Parzival.Gamification do end end + defp get_tokens_multiplier(%User{} = user) do + user = Accounts.load_user_fields(user, inventory: [:boost]) + + item = + user.inventory + |> Enum.find_value( + 1.0, + fn item -> + if item.boost.type == :tokens && + Timex.diff(DateTime.utc_now(), item.boost.expires_at, :minutes) <= 60 do + item.boost.multiplier + end + end + ) + end + def is_task_completed?(task_id, user_id) do from(t in TaskUser, where: t.task_id == ^task_id and t.user_id == ^user_id) |> Repo.exists?() diff --git a/lib/parzival/store.ex b/lib/parzival/store.ex index ff392053..dd20ca2b 100644 --- a/lib/parzival/store.ex +++ b/lib/parzival/store.ex @@ -251,7 +251,7 @@ defmodule Parzival.Store do Order.changeset(order, attrs) end - def purchase(user, product) do + def purchase_product(user, product) do Multi.new() |> Multi.update( :update_balance, @@ -265,34 +265,280 @@ defmodule Parzival.Store do |> Repo.transaction() |> case do {:ok, transaction} -> - broadcast({:ok, transaction.update_stock}, :purchased) + broadcast({:ok, transaction.update_stock}, :product_purchased) {:error, _transaction, changeset, _} -> {:error, changeset} end end - def subscribe(topic) when topic in ["purchased", "updated", "deleted"] do + def subscribe(topic) + when topic in ["product_purchased", "product_updated", "product_deleted"] do Phoenix.PubSub.subscribe(Parzival.PubSub, topic) end defp broadcast({:error, _reason} = error, _event), do: error defp broadcast({:ok, %Product{} = product}, event) - when event in [:purchased] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "purchased", {event, product.stock}) + when event in [:product_purchased] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_purchased", {event, product.stock}) {:ok, product} end defp broadcast({:ok, %Product{} = product}, event) - when event in [:updated] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "updated", {event, product}) + when event in [:product_updated] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_updated", {event, product}) {:ok, product} end defp broadcast({:ok, %Product{} = product}, event) - when event in [:deleted] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "deleted", {event, product}) + when event in [:product_deleted] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_deleted", {event, product}) {:ok, product} end + + alias Parzival.Store.Boost + + @doc """ + Returns the list of boosts. + + ## Examples + + iex> list_boosts() + [%Boost{}, ...] + + """ + def list_boosts(params \\ %{}) + + def list_boosts(opts) when is_list(opts) do + Boost + |> apply_filters(opts) + |> Repo.all() + end + + def list_boosts(flop) do + Flop.validate_and_run(Boost, flop, for: Boost) + end + + def list_boosts(%{} = flop, opts) when is_list(opts) do + Boost + |> apply_filters(opts) + |> Flop.validate_and_run(flop, for: Boost) + end + + @doc """ + Gets a single boost. + + Raises `Ecto.NoResultsError` if the Boost does not exist. + + ## Examples + + iex> get_boost!(123) + %Boost{} + + iex> get_boost!(456) + ** (Ecto.NoResultsError) + + """ + def get_boost!(id), do: Repo.get!(Boost, id) + + @doc """ + Creates a boost. + + ## Examples + + iex> create_boost(%{field: value}) + {:ok, %Boost{}} + + iex> create_boost(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_boost(attrs \\ %{}) do + %Boost{} + |> Boost.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a boost. + + ## Examples + + iex> update_boost(boost, %{field: new_value}) + {:ok, %Boost{}} + + iex> update_boost(boost, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_boost(%Boost{} = boost, attrs) do + boost + |> Boost.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a boost. + + ## Examples + + iex> delete_boost(boost) + {:ok, %Boost{}} + + iex> delete_boost(boost) + {:error, %Ecto.Changeset{}} + + """ + def delete_boost(%Boost{} = boost) do + Repo.delete(boost) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking boost changes. + + ## Examples + + iex> change_boost(boost) + %Ecto.Changeset{data: %Boost{}} + + """ + def change_boost(%Boost{} = boost, attrs \\ %{}) do + Boost.changeset(boost, attrs) + end + + alias Parzival.Store.Item + + def purchase_boost(user, boost) do + Multi.new() + |> Multi.update( + :update_balance, + User.balance_changeset(user, %{balance: user.balance - boost.price}) + ) + |> Multi.insert(:insert, %Item{user_id: user.id, boost_id: boost.id}) + |> Repo.transaction() + |> case do + {:ok, transaction} -> + {:ok, transaction} + + {:error, _transaction, changeset, _} -> + {:error, changeset} + end + end + + @doc """ + Returns the list of items. + + ## Examples + + iex> list_items() + [%Item{}, ...] + + """ + def list_items(params \\ %{}) + + def list_items(opts) when is_list(opts) do + Item + |> apply_filters(opts) + |> Repo.all() + end + + def list_items(flop) do + Flop.validate_and_run(Item, flop, for: Item) + end + + def list_items(%{} = flop, opts) when is_list(opts) do + Item + |> apply_filters(opts) + |> Flop.validate_and_run(flop, for: Item) + end + + def list_inventory(opts) when is_list(opts) do + from(i in Item, + where: is_nil(i.expires_at) or i.expires_at > ^NaiveDateTime.utc_now() + ) + |> apply_filters(opts) + |> Repo.all() + end + + @doc """ + Gets a single item. + + Raises `Ecto.NoResultsError` if the Item does not exist. + + ## Examples + + iex> get_item!(123) + %Item{} + + iex> get_item!(456) + ** (Ecto.NoResultsError) + + """ + def get_item!(id), do: Repo.get!(Item, id) + + @doc """ + Creates a item. + + ## Examples + + iex> create_item(%{field: value}) + {:ok, %Item{}} + + iex> create_item(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_item(attrs \\ %{}) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a item. + + ## Examples + + iex> update_item(item, %{field: new_value}) + {:ok, %Item{}} + + iex> update_item(item, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_item(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a item. + + ## Examples + + iex> delete_item(item) + {:ok, %Item{}} + + iex> delete_item(item) + {:error, %Ecto.Changeset{}} + + """ + def delete_item(%Item{} = item) do + Repo.delete(item) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking item changes. + + ## Examples + + iex> change_item(item) + %Ecto.Changeset{data: %Item{}} + + """ + def change_item(%Item{} = item, attrs \\ %{}) do + Item.changeset(item, attrs) + end end diff --git a/lib/parzival/store/boost.ex b/lib/parzival/store/boost.ex new file mode 100644 index 00000000..edeab8fa --- /dev/null +++ b/lib/parzival/store/boost.ex @@ -0,0 +1,53 @@ +defmodule Parzival.Store.Boost do + @moduledoc """ + A boost that can be purchased by a user. + """ + use Parzival.Schema + + alias Parzival.Store.Item + alias Parzival.Uploaders + + @types ~w(exp tokens skip_task)a + + @required_fields ~w(name description price type)a + + @optional_fields [ + :multiplier + ] + + @derive { + Flop.Schema, + filterable: [], + sortable: [:name], + compound_fields: [search: [:name]], + default_order_by: [:name], + default_order_directions: [:asc] + } + + schema "boosts" do + field :name, :string + field :description, :string + field :price, :integer + field :type, Ecto.Enum, values: @types + field :multiplier, :float + + field :image, Uploaders.BoostImage.Type + + has_many :item, Item + + timestamps() + end + + @doc false + def changeset(boost, attrs) do + boost + |> cast(attrs, @required_fields ++ @optional_fields) + |> cast_attachments(attrs, [:image]) + |> validate_required(@required_fields) + end + + def image_changeset(product, attrs) do + product + |> cast_attachments(attrs, [:image]) + end +end diff --git a/lib/parzival/store/item.ex b/lib/parzival/store/item.ex new file mode 100644 index 00000000..33b469e9 --- /dev/null +++ b/lib/parzival/store/item.ex @@ -0,0 +1,31 @@ +defmodule Parzival.Store.Item do + @moduledoc """ + A item with a boost that belongs to a user. + """ + use Parzival.Schema + + alias Parzival.Accounts.User + alias Parzival.Store.Boost + + @required_fields ~w(user_id boost_id)a + + @optional_fields [ + :expires_at + ] + + schema "items" do + field :expires_at, :naive_datetime + + belongs_to :user, User + belongs_to :boost, Boost + + timestamps() + end + + @doc false + def changeset(item, attrs) do + item + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/parzival/store/order.ex b/lib/parzival/store/order.ex index 0aa7a33f..cede73d6 100644 --- a/lib/parzival/store/order.ex +++ b/lib/parzival/store/order.ex @@ -26,7 +26,6 @@ defmodule Parzival.Store.Order do field :redeemed, :boolean, default: false belongs_to :user, User - belongs_to :product, Product timestamps() diff --git a/lib/parzival/store/product.ex b/lib/parzival/store/product.ex index dbd10661..9167ef70 100644 --- a/lib/parzival/store/product.ex +++ b/lib/parzival/store/product.ex @@ -29,7 +29,7 @@ defmodule Parzival.Store.Product do field :stock, :integer field :max_per_user, :integer - field :image, Uploaders.ProductImage.Type + field :image, Uploaders.BoostImage.Type has_many :orders, Order diff --git a/lib/parzival/uploaders/boost_image.ex b/lib/parzival/uploaders/boost_image.ex new file mode 100644 index 00000000..00fe191e --- /dev/null +++ b/lib/parzival/uploaders/boost_image.ex @@ -0,0 +1,45 @@ +defmodule Parzival.Uploaders.BoostImage do + @moduledoc """ + BoostImage is used for boost images. + """ + + use Waffle.Definition + use Waffle.Ecto.Definition + + alias Parzival.Store.Product + + @versions [:original, :medium, :thumb] + @extension_whitelist ~w(.jpg .jpeg .gif .png) + + def validate({file, _}) do + file.file_name + |> Path.extname() + |> String.downcase() + |> then(&Enum.member?(@extension_whitelist, &1)) + |> case do + true -> :ok + false -> {:error, "invalid file type"} + end + end + + def transform(:thumb, _) do + {:convert, "-strip -thumbnail 100x150^ -gravity center -extent 100x150 -format png", :png} + end + + def transform(:medium, _) do + {:convert, "-strip -thumbnail 400x600^ -gravity center -extent 400x600 -format png", :png} + end + + def filename(version, _) do + version + end + + def storage_dir(_version, {_file, %Product{} = scope}) do + "uploads/store/#{scope.id}" + end + + # Provide a default URL if there hasn't been a file uploaded + def default_url(version) do + "/images/defaults/store/boost_image_#{version}.png" + end +end diff --git a/lib/parzival_web/components/boost.ex b/lib/parzival_web/components/boost.ex new file mode 100644 index 00000000..5dcd3449 --- /dev/null +++ b/lib/parzival_web/components/boost.ex @@ -0,0 +1,42 @@ +defmodule ParzivalWeb.Components.Boost do + @moduledoc false + use ParzivalWeb, :live_component + + alias Parzival.Uploaders + alias Parzival.Store + + def render(assigns) do + ~H""" +
+ <%= if ! assigns.item.expires_at do %> + <%= if assigns.item.boost.type in [:exp, :tokens] do %> + + <% else %> + + <% end %> + <% else %> +
+ +
+
+ <% end %> +
+ """ + end + + def handle_event("activate_boost", %{"item_id" => item_id}, socket) do + item = Store.get_item!(item_id) + + Store.update_item(item, %{expires_at: Timex.shift(NaiveDateTime.utc_now(), minutes: 1)}) + # Store.update_item(item, %{expires_at: Timex.shift(NaiveDateTime.utc_now(), hours: 1)}) + + {:noreply, + socket + |> assign( + inventory: Store.list_inventory(where: [user_id: item.user_id], preloads: [:boost]) + ) + |> IO.inspect()} + end +end diff --git a/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex b/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex index 6824ed10..ae972a96 100644 --- a/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex +++ b/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex @@ -14,7 +14,7 @@ defmodule ParzivalWeb.App.LeaderboardLive.Index do @impl true def handle_params(params, _url, socket) do - user = Accounts.get_user!(socket.assigns.current_user.id, [:missions]) + user = Accounts.get_user!(socket.assigns.current_user.id, [:missions, inventory: [:boost]]) {:noreply, socket diff --git a/lib/parzival_web/live/app/jobs/offer_live/index.html.heex b/lib/parzival_web/live/app/jobs/offer_live/index.html.heex index ef3ab9c3..23a7df32 100644 --- a/lib/parzival_web/live/app/jobs/offer_live/index.html.heex +++ b/lib/parzival_web/live/app/jobs/offer_live/index.html.heex @@ -1,4 +1,4 @@ -
+

Jobs

@@ -7,10 +7,10 @@ <%= if @current_user.role in [:attendee] do %> <% end %> diff --git a/lib/parzival_web/live/app/store/boost_live/index.ex b/lib/parzival_web/live/app/store/boost_live/index.ex new file mode 100644 index 00000000..d9527ce0 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/index.ex @@ -0,0 +1,34 @@ +defmodule ParzivalWeb.App.BoostLive.Index do + @moduledoc false + use ParzivalWeb, :live_view + + import ParzivalWeb.Components.Pagination + + alias Parzival.Store + alias Parzival.Uploaders + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, + socket + |> assign(:current_page, :store) + |> assign(:page_title, "Store") + |> assign(:params, params) + |> assign(list_boosts(params))} + end + + defp list_boosts(params) do + case Store.list_boosts(params) do + {:ok, {boosts, meta}} -> + %{boosts: boosts, meta: meta} + + {:error, flop} -> + %{boosts: [], meta: flop} + end + end +end diff --git a/lib/parzival_web/live/app/store/boost_live/index.html.heex b/lib/parzival_web/live/app/store/boost_live/index.html.heex new file mode 100644 index 00000000..ec39372f --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/index.html.heex @@ -0,0 +1,49 @@ +
+
+

Store

+ +
+
+ +
+ <%= if @current_user.role in [:admin] do %> +
+ <%= live_patch("New", to: Routes.offer_new_path(@socket, :new), class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %> +
+ <% end %> +
+
+
+
    + <%= for boost <- @boosts do %> + <%= live_patch to: Routes.boost_show_path(@socket, :show, boost) do %> +
  • +
    + +
    +
    +

    + + <%= boost.name %> +

    +

    + <%= boost.description %> +

    +

    + 💰 <%= boost.price %> +

    +
    +
  • + <% end %> + <% end %> +
+ <.pagination items={@boosts} meta={@meta} params={@params} class="flex justify-between items-center w-full" /> +
+
diff --git a/lib/parzival_web/live/app/store/boost_live/show.ex b/lib/parzival_web/live/app/store/boost_live/show.ex new file mode 100644 index 00000000..9f04cfb0 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/show.ex @@ -0,0 +1,85 @@ +defmodule ParzivalWeb.App.BoostLive.Show do + @moduledoc false + use ParzivalWeb, :live_view + + alias Parzival.Accounts + alias Parzival.Store + alias Parzival.Uploaders + + @impl true + def mount(%{"id" => id}, _session, socket) do + {:ok, assign(socket, :id, id)} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, reload(socket)} + end + + @impl true + def handle_event("delete", _payload, socket) do + case Store.delete_product(socket.assigns.product) do + {:ok, _product} -> + {:noreply, + socket + |> put_flash(:success, "Product deleted successfully!") + |> push_redirect(to: Routes.product_index_path(socket, :index))} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, + socket + |> put_flash(:error, elem(changeset.errors[:orders], 0)) + |> assign(:changeset, changeset)} + end + end + + @impl true + def handle_event("purchase", _payload, socket) do + boost = socket.assigns.boost + current_user = socket.assigns.current_user + + case Store.purchase_boost(current_user, boost) do + {:ok, _boost} -> + {:noreply, + socket + |> put_flash(:success, "Boost purchased successfully!") + |> reload()} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + @impl true + def handle_info({event, _changes}, socket) when event in [:boost_purchased, :boost_updated] do + {:noreply, reload(socket)} + end + + def handle_info({event, _changes}, socket) when event in [:boost_deleted] do + {:noreply, push_redirect(socket, to: Routes.product_index_path(socket, :index))} + end + + defp reload(socket) do + id = socket.assigns.id + + socket + |> assign(:current_page, :store) + |> assign( + inventory: + Store.list_inventory(where: [user_id: socket.assigns.current_user.id], preloads: [:boost]) + ) + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:boost, Store.get_boost!(id)) + |> assign( + current_user: + Accounts.get_user!(socket.assigns.current_user.id, [ + :company, + :missions, + inventory: [:boost] + ]) + ) + end + + defp page_title(:show), do: "Show Boost" + defp page_title(:edit), do: "Edit Boost" +end diff --git a/lib/parzival_web/live/app/store/boost_live/show.html.heex b/lib/parzival_web/live/app/store/boost_live/show.html.heex new file mode 100644 index 00000000..f5e576b9 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/show.html.heex @@ -0,0 +1,68 @@ +
+
+
+

Store

+ + <%= if @current_user.role in [:admin] do %> +
+ + + <%= link("Delete", to: "#", phx_click: "delete", data: [confirm: "Are you sure?"], class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %> +
+ <% end %> +
+
+ +
+
+ <%= live_redirect("< Back", to: Routes.boost_index_path(@socket, :index), class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> +
+
+ +
+
+

+ <%= @boost.name %> +

+
+
+
+

+ 💰 <%= @boost.price %> +

+
+
+

+ <%= @boost.description %> +

+
+
+
+
+
+ +
+
+
+
+

boost options

+
+ <%= if @current_user.role == :attendee do %> + <%= if @current_user.balance >= @boost.price && length(@inventory) < 5 do %> + + <% else %> +
+ Purchase +
+ <% end %> + <% end %> +
+
+
+
+
+
diff --git a/lib/parzival_web/live/app/store/product_live/index.html.heex b/lib/parzival_web/live/app/store/product_live/index.html.heex index 91dfb521..8badb0b5 100644 --- a/lib/parzival_web/live/app/store/product_live/index.html.heex +++ b/lib/parzival_web/live/app/store/product_live/index.html.heex @@ -1,38 +1,49 @@ -
-
-
-

Store

- - <%= if @current_user.role == :admin do %> +
+
+

Store

+ +
+
+ +
+ <%= if @current_user.role in [:admin] do %>
<%= live_patch("New", to: Routes.admin_product_new_path(@socket, :new), class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %>
<% end %>
- -
    - <%= for product <- @products do %> - <%= live_patch to: Routes.product_show_path(@socket, :show, product) do %> -
  • -
    - -
    -
    -

    - - <%= product.name %> -

    -

    - <%= product.description %> -

    -

    - 💰 <%= product.price %> -

    -
    -
  • +
    +
      + <%= for product <- @products do %> + <%= live_patch to: Routes.product_show_path(@socket, :show, product) do %> +
    • +
      + +
      +
      +

      + + <%= product.name %> +

      +

      + <%= product.description %> +

      +

      + 💰 <%= product.price %> +

      +
      +
    • + <% end %> <% end %> - <% end %> -
    - <.pagination items={@products} meta={@meta} params={@params} class="flex justify-between items-center w-full" /> +
+ <.pagination items={@products} meta={@meta} params={@params} class="flex justify-between items-center w-full" /> +
diff --git a/lib/parzival_web/live/app/store/product_live/show.ex b/lib/parzival_web/live/app/store/product_live/show.ex index 67bc4152..2664aef9 100644 --- a/lib/parzival_web/live/app/store/product_live/show.ex +++ b/lib/parzival_web/live/app/store/product_live/show.ex @@ -9,9 +9,9 @@ defmodule ParzivalWeb.App.ProductLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do if connected?(socket) do - Store.subscribe("purchased") - Store.subscribe("updated") - Store.subscribe("deleted") + Store.subscribe("product_purchased") + Store.subscribe("product_updated") + Store.subscribe("product_deleted") end {:ok, assign(socket, :id, id)} @@ -60,7 +60,7 @@ defmodule ParzivalWeb.App.ProductLive.Show do product = socket.assigns.product current_user = socket.assigns.current_user - case Store.purchase(current_user, product) do + case Store.purchase_product(current_user, product) do {:ok, _product} -> {:noreply, socket @@ -73,11 +73,12 @@ defmodule ParzivalWeb.App.ProductLive.Show do end @impl true - def handle_info({event, _changes}, socket) when event in [:purchased, :updated] do + def handle_info({event, _changes}, socket) + when event in [:product_purchased, :product_updated] do {:noreply, reload(socket)} end - def handle_info({event, _changes}, socket) when event in [:deleted] do + def handle_info({event, _changes}, socket) when event in [:product_deleted] do {:noreply, push_redirect(socket, to: Routes.product_index_path(socket, :index))} end @@ -90,7 +91,12 @@ defmodule ParzivalWeb.App.ProductLive.Show do |> assign(:redeem_quantity, redeem_quantity(socket.assigns.current_user.id, id)) |> assign(:product, Store.get_product!(id)) |> assign( - current_user: Accounts.get_user!(socket.assigns.current_user.id, [:company, :missions]) + current_user: + Accounts.get_user!(socket.assigns.current_user.id, [ + :company, + :missions, + inventory: [:boost] + ]) ) end diff --git a/lib/parzival_web/live/app/vault/order_live/index.html.heex b/lib/parzival_web/live/app/vault/order_live/index.html.heex index bca71058..a0fdb411 100644 --- a/lib/parzival_web/live/app/vault/order_live/index.html.heex +++ b/lib/parzival_web/live/app/vault/order_live/index.html.heex @@ -97,19 +97,6 @@
- -
-
-
-

-

-
-
-
-

-
    -
    -
    <% end %>
    diff --git a/lib/parzival_web/live/backoffice/store/item_live/form_component.ex b/lib/parzival_web/live/backoffice/store/item_live/form_component.ex new file mode 100644 index 00000000..3c367c33 --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/item_live/form_component.ex @@ -0,0 +1,55 @@ +defmodule ParzivalWeb.ItemLive.FormComponent do + use ParzivalWeb, :live_component + + alias Parzival.Store + + @impl true + def update(%{item: item} = assigns, socket) do + changeset = Store.change_item(item) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"item" => item_params}, socket) do + changeset = + socket.assigns.item + |> Store.change_item(item_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"item" => item_params}, socket) do + save_item(socket, socket.assigns.action, item_params) + end + + defp save_item(socket, :edit, item_params) do + case Store.update_item(socket.assigns.item, item_params) do + {:ok, _item} -> + {:noreply, + socket + |> put_flash(:info, "Item updated successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_item(socket, :new, item_params) do + case Store.create_item(item_params) do + {:ok, _item} -> + {:noreply, + socket + |> put_flash(:info, "Item created successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/lib/parzival_web/live/backoffice/store/item_live/form_component.html.heex b/lib/parzival_web/live/backoffice/store/item_live/form_component.html.heex new file mode 100644 index 00000000..618cc958 --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/item_live/form_component.html.heex @@ -0,0 +1,9 @@ +
    +

    <%= @title %>

    + + <.form let={f} for={@changeset} id="item-form" phx-target={@myself} phx-change="validate" phx-submit="save"> +
    + <%= submit("Save", phx_disable_with: "Saving...") %> +
    + +
    diff --git a/lib/parzival_web/live/hooks.ex b/lib/parzival_web/live/hooks.ex index 0b1bbd4b..eee88bca 100644 --- a/lib/parzival_web/live/hooks.ex +++ b/lib/parzival_web/live/hooks.ex @@ -5,12 +5,13 @@ defmodule ParzivalWeb.Hooks do import Phoenix.LiveView alias Parzival.Accounts + alias Parzival.Store def on_mount(:default, _params, _session, socket) do {:cont, assign(socket, :page_title, "JOIN")} end - def on_mount(:current_user, _params, %{"user_token" => user_token}, socket) do + def on_mount(:current_user, params, %{"user_token" => user_token}, socket) do current_user = Accounts.get_user_by_session_token(user_token) if is_nil(current_user) do @@ -18,6 +19,9 @@ defmodule ParzivalWeb.Hooks do else {:cont, socket + |> assign( + inventory: Store.list_inventory(where: [user_id: current_user.id], preloads: [:boost]) + ) |> assign(current_user: Accounts.load_user_fields(current_user, [:company, :missions]))} end end diff --git a/lib/parzival_web/router.ex b/lib/parzival_web/router.ex index 0f26bace..9694f1a6 100644 --- a/lib/parzival_web/router.ex +++ b/lib/parzival_web/router.ex @@ -70,6 +70,9 @@ defmodule ParzivalWeb.Router do live "/store/", ProductLive.Index, :index live "/store/:id", ProductLive.Show, :show + live "/boosts/", BoostLive.Index, :index + live "/boosts/:id", BoostLive.Show, :show + live "/vault", OrderLive.Index, :index live "/vault/:id", OrderLive.Show, :show diff --git a/lib/parzival_web/templates/layout/live.html.heex b/lib/parzival_web/templates/layout/live.html.heex index c09c5cc2..3ee033fa 100644 --- a/lib/parzival_web/templates/layout/live.html.heex +++ b/lib/parzival_web/templates/layout/live.html.heex @@ -120,12 +120,32 @@
    <% end %> - <% end %> -
    - <%= for i <- 1..12 do %> -
    +

    + <%= "Inventory (#{length(@inventory)}/5)" %> +

    +
    + <%= for item <- @inventory do %> + <.live_component module={ParzivalWeb.Components.Boost} id={item.id} item={item} /> + <% end %> + <%= if length(@inventory) < 5 do %> + <%= for i <- 1..5-length(@inventory) do %> +
    + <% end %> + <% end %> +
    + <%= for item <- @inventory do %> + <%= if item.expires_at do %> +
    +
    + <%= item.boost.name %> +
    +
    + <%= relative_datetime_in_digital_clock_format(item.expires_at) %> +
    +
    + <% end %> <% end %> -
    + <% end %>