diff --git a/assets/css/app.css b/assets/css/app.css index 465a4af..34f98a1 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -220,3 +220,8 @@ h6 { .hover\:text-plum:hover { color: var(--color-primary); } + +/* Fix label-text color in dark mode */ +.label-text { + color: var(--color-base-content); +} diff --git a/lib/spazio_solazzo/accounts/user.ex b/lib/spazio_solazzo/accounts/user.ex index 27af03c..aecf91b 100644 --- a/lib/spazio_solazzo/accounts/user.ex +++ b/lib/spazio_solazzo/accounts/user.ex @@ -135,6 +135,16 @@ defmodule SpazioSolazzo.Accounts.User do end end + # Make sure it is loaded in all read actions + preparations do + prepare build(load: [:is_admin]) + end + + # Make sure it is loaded in all write actions + changes do + change load(:is_admin) + end + attributes do uuid_primary_key :id @@ -152,6 +162,17 @@ defmodule SpazioSolazzo.Accounts.User do allow_nil? true public? true end + + attribute :role, :atom do + allow_nil? false + public? false + constraints one_of: [:customer, :admin] + default :customer + end + end + + calculations do + calculate :is_admin, :boolean, expr(role == :admin) end identities do diff --git a/lib/spazio_solazzo_web/components/admin_components.ex b/lib/spazio_solazzo_web/components/admin_components.ex new file mode 100644 index 0000000..44a392c --- /dev/null +++ b/lib/spazio_solazzo_web/components/admin_components.ex @@ -0,0 +1,38 @@ +defmodule SpazioSolazzoWeb.AdminComponents do + @moduledoc """ + Reusable components for admin pages (dashboard & tools). + """ + use Phoenix.Component + + import SpazioSolazzoWeb.CoreComponents, only: [icon: 1, button: 1] + import Phoenix.Component + + use Phoenix.VerifiedRoutes, + endpoint: SpazioSolazzoWeb.Endpoint, + router: SpazioSolazzoWeb.Router, + statics: SpazioSolazzoWeb.static_paths() + + @doc """ + Cards displayed for each tool available to admins + """ + attr :id, :string, doc: "optional id for the tool" + attr :title, :string + attr :icon, :string, doc: "Icon used to represent the type of tool" + attr :description, :string + + def tool_card(assigns) do + ~H""" +
+
+

+ <.icon name={@icon} class="size-6 text-secondary" /> {@title} +

+

{@description}

+
+ <.button class="btn btn-primary btn-sm">Open +
+
+
+ """ + end +end diff --git a/lib/spazio_solazzo_web/components/core_components.ex b/lib/spazio_solazzo_web/components/core_components.ex index 984cf1d..5bdea1d 100644 --- a/lib/spazio_solazzo_web/components/core_components.ex +++ b/lib/spazio_solazzo_web/components/core_components.ex @@ -31,6 +31,26 @@ defmodule SpazioSolazzoWeb.CoreComponents do alias Phoenix.LiveView.JS + @doc """ + Renders a back-to navigation link + """ + + attr :navigate, :string + attr :value, :string + attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" + + def back_to_link(assigns) do + ~H""" + <.link + class="mb-6 text-sm font-medium text-neutral hover:text-secondary transition-colors inline-flex items-center gap-2" + navigate={@navigate} + {@rest} + > + <.icon name="hero-arrow-left" class="w-5 h-5" />{@value} + + """ + end + @doc """ Renders flash notices. @@ -223,7 +243,8 @@ defmodule SpazioSolazzoWeb.CoreComponents do checked={@checked} class={@class || "checkbox checkbox-sm"} {@rest} - />{@label} + /> + {@label} <.error :for={msg <- @errors}>{msg} diff --git a/lib/spazio_solazzo_web/components/landing_components.ex b/lib/spazio_solazzo_web/components/landing_components.ex index 1371817..4a132eb 100644 --- a/lib/spazio_solazzo_web/components/landing_components.ex +++ b/lib/spazio_solazzo_web/components/landing_components.ex @@ -4,7 +4,7 @@ defmodule SpazioSolazzoWeb.LandingComponents do """ use Phoenix.Component - import SpazioSolazzoWeb.CoreComponents, only: [icon: 1] + import SpazioSolazzoWeb.CoreComponents, only: [icon: 1, back_to_link: 1] import Phoenix.Component use Phoenix.VerifiedRoutes, @@ -131,14 +131,10 @@ defmodule SpazioSolazzoWeb.LandingComponents do ~H"""
-
- <.link - navigate={~p"/"} - class="hover:text-primary transition-colors flex items-center gap-1" - > - <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Home - -
+ <.back_to_link + navigate={~p"/"} + value="Back to Home" + />
diff --git a/lib/spazio_solazzo_web/components/layouts.ex b/lib/spazio_solazzo_web/components/layouts.ex index 3513ccf..75961cd 100644 --- a/lib/spazio_solazzo_web/components/layouts.ex +++ b/lib/spazio_solazzo_web/components/layouts.ex @@ -38,8 +38,85 @@ defmodule SpazioSolazzoWeb.Layouts do slot :inner_block, required: true def app(assigns) do + current_year = Date.utc_today().year + + assigns = assign(assigns, :current_year, current_year) + ~H""" - <.app_header current_user={@current_user} /> +
{render_slot(@inner_block)} @@ -47,156 +124,6 @@ defmodule SpazioSolazzoWeb.Layouts do <.flash_group flash={@flash} /> - <.footer /> - """ - end - - @doc """ - Shows the flash group with standard titles and content. - - ## Examples - - <.flash_group flash={@flash} /> - """ - attr :flash, :map, required: true, doc: "the map of flash messages" - attr :id, :string, default: "flash-group", doc: "the optional id of flash container" - - def flash_group(assigns) do - ~H""" -
- <.flash kind={:info} flash={@flash} /> - <.flash kind={:error} flash={@flash} /> - - <.flash - id="client-error" - kind={:error} - title={gettext("We can't find the internet")} - phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")} - phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})} - hidden - > - {gettext("Attempting to reconnect")} - <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> - - - <.flash - id="server-error" - kind={:error} - title={gettext("Something went wrong!")} - phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")} - phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})} - hidden - > - {gettext("Attempting to reconnect")} - <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> - -
- """ - end - - @doc """ - Provides dark vs light theme toggle based on themes defined in app.css. - - See in root.html.heex which applies the theme before page load. - """ - def theme_toggle(assigns) do - ~H""" - - """ - end - - attr :current_user, :map, - default: nil, - doc: "the current authenticated user" - - defp app_header(assigns) do - ~H""" -
-
- <.link navigate="/" class="flex items-center gap-3 hover:opacity-80 transition-opacity"> - Spazio Solazzo - - -
- <.theme_toggle /> - <%= if @current_user do %> - <%!-- Desktop menu --%> - - <%!-- Mobile menu button --%> - - <% else %> - <.link navigate={~p"/sign-in"} id="sign-in-link" class="btn btn-secondary btn-sm"> - Sign In - - <% end %> -
-
- <%!-- Mobile dropdown menu --%> - <%= if @current_user do %> - - <% end %> -
- """ - end - - defp footer(assigns) do - current_year = Date.utc_today().year - - assigns = assign(assigns, :current_year, current_year) - - ~H"""
@@ -286,4 +213,88 @@ defmodule SpazioSolazzoWeb.Layouts do
""" end + + @doc """ + Shows the flash group with standard titles and content. + + ## Examples + + <.flash_group flash={@flash} /> + """ + attr :flash, :map, required: true, doc: "the map of flash messages" + attr :id, :string, default: "flash-group", doc: "the optional id of flash container" + + def flash_group(assigns) do + ~H""" +
+ <.flash kind={:info} flash={@flash} /> + <.flash kind={:error} flash={@flash} /> + + <.flash + id="client-error" + kind={:error} + title={gettext("We can't find the internet")} + phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + + + <.flash + id="server-error" + kind={:error} + title={gettext("Something went wrong!")} + phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + +
+ """ + end + + @doc """ + Provides dark vs light theme toggle based on themes defined in app.css. + + See in root.html.heex which applies the theme before page load. + """ + def theme_toggle(assigns) do + ~H""" + + """ + end + + defp menu_svg(assigns) do + ~H""" + + + + """ + end end diff --git a/lib/spazio_solazzo_web/live/admin/dashboard_live.ex b/lib/spazio_solazzo_web/live/admin/dashboard_live.ex new file mode 100644 index 0000000..b8085b5 --- /dev/null +++ b/lib/spazio_solazzo_web/live/admin/dashboard_live.ex @@ -0,0 +1,21 @@ +defmodule SpazioSolazzoWeb.Admin.DashboardLive do + @moduledoc """ + Admin dashboard home page. Lists the available tools that admins have + """ + + use SpazioSolazzoWeb, :live_view + + alias SpazioSolazzo.BookingSystem + import SpazioSolazzoWeb.AdminComponents + + def mount(_params, _session, socket) do + {:ok, coworking_space} = BookingSystem.get_space_by_slug("coworking", not_found_error?: false) + {:ok, meeting_space} = BookingSystem.get_space_by_slug("meeting", not_found_error?: false) + + {:ok, + assign(socket, + coworking_space: coworking_space, + meeting_space: meeting_space + )} + end +end diff --git a/lib/spazio_solazzo_web/live/admin/dashboard_live.html.heex b/lib/spazio_solazzo_web/live/admin/dashboard_live.html.heex new file mode 100644 index 0000000..1fd9803 --- /dev/null +++ b/lib/spazio_solazzo_web/live/admin/dashboard_live.html.heex @@ -0,0 +1,28 @@ + +
+ <.back_to_link + navigate={~p"/"} + value="Back to Home" + /> + +

Admin Dashboard

+ +
+ <%= if @meeting_space do %> + <.tool_card + title={@meeting_space.name} + description="Create walk-in bookings for the space" + icon="hero-user-group" + /> + <% end %> + + <%= if @coworking_space do %> + <.tool_card + title={@coworking_space.name} + description="Create walk-in bookings for the space" + icon="hero-user-group" + /> + <% end %> +
+
+
diff --git a/lib/spazio_solazzo_web/live/booking/asset_booking_live.html.heex b/lib/spazio_solazzo_web/live/booking/asset_booking_live.html.heex index 1a319c6..91abd08 100644 --- a/lib/spazio_solazzo_web/live/booking/asset_booking_live.html.heex +++ b/lib/spazio_solazzo_web/live/booking/asset_booking_live.html.heex @@ -1,12 +1,10 @@
-
- <.link +
+ <.back_to_link navigate={"/#{@space.slug}"} - class="inline-flex items-center gap-2 text-sm font-medium text-neutral hover:text-secondary transition-colors" - > - <.icon name="hero-arrow-left" class="w-5 h-5" /> Back to {@space.name} - + value={"Back to #{@space.name}"} + />
diff --git a/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex b/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex index 934bb3e..0f56df5 100644 --- a/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex +++ b/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex @@ -118,14 +118,14 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
diff --git a/lib/spazio_solazzo_web/live/page_live.html.heex b/lib/spazio_solazzo_web/live/page_live.html.heex index 1264d62..ee82704 100644 --- a/lib/spazio_solazzo_web/live/page_live.html.heex +++ b/lib/spazio_solazzo_web/live/page_live.html.heex @@ -1,6 +1,6 @@
-
+
@@ -100,6 +100,6 @@ <% end %>
-
+
diff --git a/lib/spazio_solazzo_web/live/user/profile_live.html.heex b/lib/spazio_solazzo_web/live/user/profile_live.html.heex index c9458ba..d8d22d5 100644 --- a/lib/spazio_solazzo_web/live/user/profile_live.html.heex +++ b/lib/spazio_solazzo_web/live/user/profile_live.html.heex @@ -1,5 +1,12 @@
+
+ <.back_to_link + navigate={~p"/"} + value="Back to Home" + /> +
+

User Profile @@ -71,7 +78,7 @@
@@ -165,7 +172,7 @@ diff --git a/lib/spazio_solazzo_web/live_user_auth.ex b/lib/spazio_solazzo_web/live_user_auth.ex index 3e9e906..f4c95f2 100644 --- a/lib/spazio_solazzo_web/live_user_auth.ex +++ b/lib/spazio_solazzo_web/live_user_auth.ex @@ -36,4 +36,14 @@ defmodule SpazioSolazzoWeb.LiveUserAuth do {:cont, assign(socket, :current_user, nil)} end end + + def on_mount(:live_admin_required, _params, _session, socket) do + case socket.assigns[:current_user] do + %{is_admin: true} -> + {:cont, socket} + + _ -> + {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")} + end + end end diff --git a/lib/spazio_solazzo_web/router.ex b/lib/spazio_solazzo_web/router.ex index 787b619..59397de 100644 --- a/lib/spazio_solazzo_web/router.ex +++ b/lib/spazio_solazzo_web/router.ex @@ -56,6 +56,13 @@ defmodule SpazioSolazzoWeb.Router do live "/book/asset/:asset_id", AssetBookingLive live "/profile", ProfileLive end + + ash_authentication_live_session :admin_routes, + on_mount: [ + {SpazioSolazzoWeb.LiveUserAuth, :live_admin_required} + ] do + live "/admin/dashboard", Admin.DashboardLive + end end # Enable LiveDashboard and Swoosh mailbox preview in development diff --git a/priv/repo/migrations/20260118203753_add_role_to_users.exs b/priv/repo/migrations/20260118203753_add_role_to_users.exs new file mode 100644 index 0000000..72fa527 --- /dev/null +++ b/priv/repo/migrations/20260118203753_add_role_to_users.exs @@ -0,0 +1,21 @@ +defmodule SpazioSolazzo.Repo.Migrations.AddRoleToUsers do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:users) do + add :role, :text, null: false, default: "customer" + end + end + + def down do + alter table(:users) do + remove :role + end + end +end diff --git a/priv/resource_snapshots/repo/users/20260118203753.json b/priv/resource_snapshots/repo/users/20260118203753.json new file mode 100644 index 0000000..abb00bd --- /dev/null +++ b/priv/resource_snapshots/repo/users/20260118203753.json @@ -0,0 +1,94 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "citext" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": false, + "default": "\"customer\"", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "role", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "0B279BA9251EA91451BB80DF86AF81ECCA5395011864BE01BE1C369420AD9C18", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "users_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.SpazioSolazzo.Repo", + "schema": null, + "table": "users" +} \ No newline at end of file