Add default organization feature
Features: - Users can set a default organization in org settings - First organization created is automatically set as default - Default org selector only shown if user has 2+ organizations - Migration auto-sets first org as default for existing users Database changes: - Added is_default boolean field to organization_memberships - Added partial index for efficient default org lookups - Migration sets oldest org as default for each existing user UI changes: - Organization settings page shows default org toggle - Green badge displayed when org is already default - Button to set org as default if it's not current default Backend: - Organizations.set_default_organization/2 manages defaults - Organizations.get_default_membership/1 retrieves default - Auto-set first org as default in create_organization/3
This commit is contained in:
parent
7a1317d0d0
commit
fc66109f19
5 changed files with 162 additions and 15 deletions
|
|
@ -93,6 +93,13 @@ defmodule Towerops.Organizations do
|
|||
end
|
||||
|
||||
defp do_create_organization(attrs, user_id) do
|
||||
# Check if user has any existing organizations
|
||||
has_existing_orgs =
|
||||
Repo.exists?(
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id
|
||||
)
|
||||
|
||||
multi =
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.insert(:organization, Organization.changeset(%Organization{}, attrs))
|
||||
|
|
@ -100,27 +107,16 @@ defmodule Towerops.Organizations do
|
|||
Membership.changeset(%Membership{}, %{
|
||||
organization_id: organization.id,
|
||||
user_id: user_id,
|
||||
role: :owner
|
||||
role: :owner,
|
||||
# Set as default if this is the user's first organization
|
||||
is_default: !has_existing_orgs
|
||||
})
|
||||
end)
|
||||
|> Ecto.Multi.run(:set_default, fn _repo, %{organization: organization} ->
|
||||
# Set this as the user's default organization if they don't have one
|
||||
user = Towerops.Accounts.get_user!(user_id)
|
||||
|
||||
if user.default_organization_id == nil do
|
||||
user
|
||||
|> Ecto.Changeset.change(default_organization_id: organization.id)
|
||||
|> Repo.update()
|
||||
else
|
||||
{:ok, user}
|
||||
end
|
||||
end)
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, %{organization: organization}} -> {:ok, organization}
|
||||
{:error, :organization, changeset, _} -> {:error, changeset}
|
||||
{:error, :membership, changeset, _} -> {:error, changeset}
|
||||
{:error, :set_default, changeset, _} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -273,6 +269,52 @@ defmodule Towerops.Organizations do
|
|||
Repo.delete(membership)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the user's default organization membership.
|
||||
"""
|
||||
def get_default_membership(user_id) do
|
||||
Repo.one(
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id and m.is_default == true,
|
||||
preload: [:organization]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets an organization as the user's default.
|
||||
Clears any existing default and sets the new one.
|
||||
"""
|
||||
def set_default_organization(user_id, organization_id) do
|
||||
# Verify user has access to this organization
|
||||
if user_has_access?(user_id, organization_id) do
|
||||
multi =
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.update_all(:clear_old_default, clear_default_query(user_id), set: [is_default: false])
|
||||
|> Ecto.Multi.update_all(
|
||||
:set_new_default,
|
||||
set_default_query(user_id, organization_id),
|
||||
set: [is_default: true]
|
||||
)
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, _} -> {:ok, get_organization!(organization_id)}
|
||||
{:error, _, changeset, _} -> {:error, changeset}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp clear_default_query(user_id) do
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id and m.is_default == true
|
||||
end
|
||||
|
||||
defp set_default_query(user_id, organization_id) do
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id and m.organization_id == ^organization_id
|
||||
end
|
||||
|
||||
## Invitations
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ defmodule Towerops.Organizations.Membership do
|
|||
@foreign_key_type :binary_id
|
||||
schema "organization_memberships" do
|
||||
field :role, Ecto.Enum, values: [:owner, :admin, :member, :viewer]
|
||||
field :is_default, :boolean, default: false
|
||||
|
||||
belongs_to :organization, Organization
|
||||
belongs_to :user, User
|
||||
|
|
@ -26,6 +27,7 @@ defmodule Towerops.Organizations.Membership do
|
|||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
role: :owner | :admin | :member | :viewer,
|
||||
is_default: boolean(),
|
||||
organization_id: Ecto.UUID.t(),
|
||||
organization: NotLoaded.t() | Organization.t(),
|
||||
user_id: Ecto.UUID.t(),
|
||||
|
|
@ -37,7 +39,7 @@ defmodule Towerops.Organizations.Membership do
|
|||
@doc false
|
||||
def changeset(membership, attrs) do
|
||||
membership
|
||||
|> cast(attrs, [:role, :organization_id, :user_id])
|
||||
|> cast(attrs, [:role, :organization_id, :user_id, :is_default])
|
||||
|> validate_required([:role, :organization_id, :user_id])
|
||||
|> foreign_key_constraint(:organization_id)
|
||||
|> foreign_key_constraint(:user_id)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ defmodule ToweropsWeb.Org.SettingsLive do
|
|||
# Get assignment breakdown to show impact
|
||||
assignment_breakdown = Agents.Stats.get_device_assignment_breakdown(organization.id)
|
||||
|
||||
# Get user's membership for this org to check if it's default
|
||||
membership = Organizations.get_membership(organization.id, user.id)
|
||||
|
||||
# Count user's total organizations
|
||||
user_orgs_count = user.id |> Organizations.list_user_organizations() |> length()
|
||||
|
||||
# Log organization data access
|
||||
AuditLogger.log_org_data_accessed(nil, user.id, organization.id, "view_settings")
|
||||
|
||||
|
|
@ -23,6 +29,8 @@ defmodule ToweropsWeb.Org.SettingsLive do
|
|||
{:ok,
|
||||
socket
|
||||
|> assign(:organization, organization)
|
||||
|> assign(:membership, membership)
|
||||
|> assign(:user_orgs_count, user_orgs_count)
|
||||
|> assign(:available_agents, available_agents)
|
||||
|> assign(:assignment_breakdown, assignment_breakdown)
|
||||
|> assign(:form, to_form(changeset))}
|
||||
|
|
@ -68,4 +76,29 @@ defmodule ToweropsWeb.Org.SettingsLive do
|
|||
|
||||
{:noreply, put_flash(socket, :info, "Applied default agent to #{count} device records across all sites")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_default_org", _params, socket) do
|
||||
user = socket.assigns.current_scope.user
|
||||
organization = socket.assigns.organization
|
||||
membership = socket.assigns.membership
|
||||
|
||||
if membership.is_default do
|
||||
{:noreply, put_flash(socket, :error, "This is already your default organization")}
|
||||
else
|
||||
case Organizations.set_default_organization(user.id, organization.id) do
|
||||
{:ok, _} ->
|
||||
# Refresh membership to reflect change
|
||||
updated_membership = Organizations.get_membership(organization.id, user.id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "#{organization.name} is now your default organization")
|
||||
|> assign(:membership, updated_membership)}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to set default organization")}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,6 +48,47 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @user_orgs_count > 1 do %>
|
||||
<!-- Default Organization Section -->
|
||||
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
<div>
|
||||
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
||||
Default Organization
|
||||
</h2>
|
||||
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
||||
Set this organization as your default. When you log in, you'll be directed to your default organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<%= if @membership.is_default do %>
|
||||
<div class="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4">
|
||||
<div class="flex items-center">
|
||||
<.icon
|
||||
name="hero-check-circle"
|
||||
class="h-5 w-5 text-green-600 dark:text-green-400"
|
||||
/>
|
||||
<p class="ml-3 text-sm text-green-800 dark:text-green-200">
|
||||
This is your default organization
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="toggle_default_org"
|
||||
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400"
|
||||
>
|
||||
<.icon name="hero-star" class="h-4 w-4 inline" /> Set as Default Organization
|
||||
</button>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Click to make this your default organization
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Use Sites Section -->
|
||||
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
defmodule Towerops.Repo.Migrations.AddIsDefaultToOrganizationMemberships do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:organization_memberships) do
|
||||
add :is_default, :boolean, default: false, null: false
|
||||
end
|
||||
|
||||
create index(:organization_memberships, [:user_id, :is_default],
|
||||
where: "is_default = true",
|
||||
name: :organization_memberships_user_id_default_index
|
||||
)
|
||||
|
||||
# Set the first organization for each user as their default
|
||||
execute(
|
||||
"""
|
||||
UPDATE organization_memberships
|
||||
SET is_default = true
|
||||
WHERE id IN (
|
||||
SELECT DISTINCT ON (user_id) id
|
||||
FROM organization_memberships
|
||||
ORDER BY user_id, inserted_at ASC
|
||||
)
|
||||
""",
|
||||
# Rollback: clear all defaults
|
||||
"UPDATE organization_memberships SET is_default = false"
|
||||
)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue