PhoenixKitLocations

Hex.pm License

Locations module for PhoenixKit — manage physical locations with custom types, multilingual fields, and a full admin interface.

Features

Installation

Add phoenix_kit_locations to your list of dependencies in mix.exs:

def deps do
[
{:phoenix_kit_locations, "~> 0.4"}
]
end

The module is auto-discovered by PhoenixKit — no manual router configuration needed. Run mix phoenix_kit.update: it applies core's migrations, then this module's own chain (PhoenixKitLocations.Migrations), generating a wrapper migration in your repo's migrations directory. Commit that file.

Usage

Admin Interface

Once installed, the admin dashboard adds a Locations tab with two subtabs:

Routes are registered automatically:

Path Description
/admin/locations Locations list
/admin/locations/new Create location
/admin/locations/:uuid/edit Edit location
/admin/locations/types Types list
/admin/locations/types/new Create type
/admin/locations/types/:uuid/edit Edit type

With locations.manage_all, the list shows each location's Owner and filters by All / Global / Owned, and the location form has an owner picker. Users with only locations see neither (see Permissions below).

Permissions: own locations vs. all locations

A location can belong to a user (a person or an organization account). Without an owner it is global. The same /admin/locations pages serve both kinds of user:

Permission Grants
locations The Locations pages, scoped to locations owned by the user or by the user's organization: list, create, edit, delete, Structure
locations.manage_all Every location (owned or global), assigning owners, internal notes, attachments, and the Types pages

Organizations: a person who belongs to an organization account shares that organization's locations with its other members, and the locations they create belong to the organization. Teammates at one company therefore see and manage the same warehouses. A person without an organization owns what they create.

Grant locations alone to a role (for example "Seller") so its users manage only their own (or their company's) sites; grant locations.manage_all as well to staff who manage everything. Admin receives both automatically, and Owner holds every permission.

⚠️ Upgrading: before this change, locations opened every location. A custom role that holds locations now sees only its own locations until it is also granted locations.manage_all. Admin and Owner are unaffected.

Deleting a user deletes the locations they own, with their spaces.

Programmatic Access

All business logic lives in the PhoenixKitLocations.Locations context:

alias PhoenixKitLocations.Locations
# Locations
Locations.list_locations()
Locations.create_location(%{name: "HQ", city: "Berlin", country: "DE"})
Locations.list_locations(status: "active", type_uuid: some_uuid)
# Location types
Locations.list_location_types()
Locations.create_location_type(%{name: "Office"})
# Type assignments (take location_uuid, not the struct)
Locations.sync_location_types(location.uuid, [type_uuid_1, type_uuid_2])
Locations.has_type?(location.uuid, type_uuid)
# Ownership — the owner is never taken from attrs, only from these calls
{:ok, site} = Locations.create_location(%{name: "North Depot"}, owner_uuid: user.uuid)
Locations.set_location_owner(site, other_user.uuid, actor_uuid: admin.uuid) # or nil for global
Locations.list_locations(owner_uuid: user.uuid) # only this owner's locations
Locations.list_locations(owner_uuid: PhoenixKitLocations.Policy.owner_uuids(user)) # a person + their organization
Locations.list_locations(owner_uuid: nil) # only global locations
Locations.list_locations(owner_uuid: :any) # every owned location
Locations.list_locations() # everything — never for tenant-facing pages
Locations.get_location_for_owner(uuid, user.uuid) # nil unless user owns it

PlacePicker takes the same filter as an attr and enforces it on selection. For a person and their organization, pass owner_uuid={PhoenixKitLocations.Policy.owner_uuids(@phoenix_kit_current_scope)}; leaving the attr out lists every location.

Error Handling

Non-changeset errors returned by PhoenixKitLocations.Locations are atoms (:location_not_found, :type_assignment_failed, …). At the UI boundary, LiveViews translate them via PhoenixKitLocations.Errors.message/1, which wraps each mapping in gettext so flashes render in the user's locale.

case Locations.delete_location(loc, actor_uuid: user.uuid) do
{:ok, _} -> :ok
{:error, _changeset} -> {:error, PhoenixKitLocations.Errors.message(:location_delete_failed)}
end

Activity Logging

Every mutating context function logs a business-level activity to PhoenixKit.Activity when an actor_uuid: opt is passed. Logging is fire-and-forget — failures (e.g. host hasn't run core's V90 migration) never crash the primary operation. The action format is "resource.verb": location.created, location.updated, location.deleted, location.owner_changed, location.types_synced, location_type.created, etc.

Location Features

Locations support a set of boolean feature flags stored as a JSONB map:

wheelchair_accessible, elevator, parking, public_transport, loading_dock, air_conditioning, wifi, restrooms, security, cctv

Locations.create_location(%{
name: "Warehouse",
features: %{"loading_dock" => true, "parking" => true, "cctv" => true}
})

Database Schemas

All schemas use UUIDv7 primary keys. The tables belong to this module's versioned migration chain, PhoenixKitLocations.Migrations (version tracked as a pkloc_schema:<N> comment on phoenix_kit_locations). Core's baseline also still creates them on existing installs; V1 of the chain adopts those tables without changing their shape, and rolling the chain back never drops a table.

Table Description
phoenix_kit_locations Locations with address, contact, features, multilang data, and an optional owner_uuid (chain V2)
phoenix_kit_location_types User-defined location categories
phoenix_kit_location_type_assignments Many-to-many join table
phoenix_kit_location_spaces Per-location tree of floors, rooms, zones, sections, aisles, shelves

Development

mix deps.get # Install dependencies
mix test # Run tests
mix precommit # compile + format + credo + dialyzer
mix quality # format + credo + dialyzer
mix quality.ci # format --check-formatted + credo + dialyzer

License

MIT — see LICENSE for details.