# `PhoenixKitProjects.Statuses`
[🔗](https://github.com/BeamLabEU/phoenix_kit_projects/blob/v0.14.0/lib/phoenix_kit_projects/statuses.ex#L1)

User-defined project **workflow statuses**, configured through the
optional `phoenix_kit_entities` module and cemented locally when a
project starts.

Two layers:

- **Catalog (entities).** A shared `project_status` entity (auto-seeded
  with a default vocabulary) and/or full custom per-project entities the
  user owns. Templates and not-yet-started projects read this catalog
  live, so edits to the vocabulary flow straight through.
- **Cemented (local).** When a project starts, its chosen catalog
  statuses are snapshotted into `phoenix_kit_project_statuses`
  (`PhoenixKitProjects.Schemas.ProjectStatus`). The running project then
  uses its own frozen, independently-editable copy — later catalog edits
  don't touch it. Mirrors the module's template→instance philosophy.

The selected status is addressed by its **slug** (`current_status_slug`
on the project), a stable identity that resolves against the live
catalog before start and the cemented local rows after.

## Optional dependency

`phoenix_kit_entities` is optional. Every public function degrades
gracefully when it's absent or disabled: reads return `[]`/`nil`,
provisioning returns `{:error, :entities_not_available}`, and
`cement_project_statuses/2` becomes a no-op. The guard scaffolding
(`@compile {:no_warn_undefined, …}` + `safe_call/2` + `available?/0`)
mirrors `PhoenixKitProjects.Translations`.

# `status`

```elixir
@type status() :: %{
  uuid: String.t() | nil,
  label: String.t(),
  slug: String.t(),
  color: String.t() | nil,
  position: integer()
}
```

A normalized status row, identical whether it came from the live catalog
or a cemented local row. `uuid` is the source row's uuid (entity_data
uuid pre-start, `phoenix_kit_project_statuses` uuid post-start); `slug`
is the stable cross-boundary identity.

# `add_project_status`

```elixir
@spec add_project_status(PhoenixKitProjects.Schemas.Project.t(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.ProjectStatus.t()}
  | {:error, Ecto.Changeset.t()}
```

Adds a cemented status row to a started project.

# `available?`

```elixir
@spec available?() :: boolean()
```

Is the entities-backed status feature usable right now? True when the
optional `phoenix_kit_entities` plugin is loaded AND enabled at runtime.

# `cement_project_statuses`

```elixir
@spec cement_project_statuses(
  PhoenixKitProjects.Schemas.Project.t(),
  keyword()
) :: :ok
```

Snapshots a project's chosen catalog statuses into local
`phoenix_kit_project_statuses` rows. Called inside `start_project/2`'s
transaction. Idempotent: a no-op if the project already has cemented
rows. A no-op (returns `:ok`) when entities is unavailable — a started
project simply has no workflow statuses until the module is wired up.

Runs inside the caller's transaction, so a raised error rolls the whole
start back.

# `create_default_status_entity`

```elixir
@spec create_default_status_entity(keyword()) :: {:ok, struct()} | {:error, term()}
```

Creates a **new** default status entity, seeded with the default
vocabulary, and returns `{:ok, entity}` (or `{:error, :entities_not_available}`).

Named `project_statuses`; if that's already taken it auto-increments —
`project_statuses_2`, `project_statuses_3`, … — so generating a default
again always produces a fresh, independent list rather than reusing the
existing one.

# `current_status`

```elixir
@spec current_status(PhoenixKitProjects.Schemas.Project.t()) :: status() | nil
```

The currently-selected status for a project as a normalized map, or
`nil` when unset / unresolvable (e.g. the slug points at a trashed
catalog row, or entities is unavailable).

# `default_entity_base`

```elixir
@spec default_entity_base() :: String.t()
```

Base name for generated default status entities (`"project_statuses"`).

# `default_statuses`

```elixir
@spec default_statuses() :: [map()]
```

The default seeded status vocabulary (for tests / inspection).

# `ensure_project_status_entity`

```elixir
@spec ensure_project_status_entity(
  PhoenixKitProjects.Schemas.Project.t(),
  keyword()
) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
```

Provisions a dedicated custom status entity for `project` (named
`project_status_<uuid>`), seeds it with the defaults as a starting
point, points the project's `status_entity_uuid` at it, and returns
`{:ok, project}` with the updated project.

This is the per-project opt-in. The user fully owns the resulting
entity afterward (rename, restructure fields, edit statuses) in the
entities admin. No-op-with-error when entities is unavailable.

# `get_project_status`

```elixir
@spec get_project_status(PhoenixKitProjects.Schemas.Project.t(), String.t()) ::
  PhoenixKitProjects.Schemas.ProjectStatus.t() | nil
```

Fetches a cemented status row by uuid, scoped to a project.

# `global_default_status_entity_uuid`

```elixir
@spec global_default_status_entity_uuid() :: String.t() | nil
```

The admin-chosen global default status entity uuid (the
`projects_default_status_entity_uuid` setting), or `nil` when unset. This
is what a project's "Shared default" resolves to. Set on the projects
Settings page; nothing is auto-created.

# `global_use_status_translations?`

```elixir
@spec global_use_status_translations?() :: boolean()
```

The global default for status-title translation display (setting, default true).

# `list_catalog_statuses`

```elixir
@spec list_catalog_statuses(String.t()) :: [status()]
```

Live catalog statuses for a given entity uuid (normalized). `[]` when
entities is unavailable.

# `list_project_statuses`

```elixir
@spec list_project_statuses(PhoenixKitProjects.Schemas.Project.t()) :: [status()]
```

Cemented local status rows for a project, ordered by position.

# `list_status_source_entities`

```elixir
@spec list_status_source_entities() :: [{String.t(), [{String.t(), String.t()}]}]
```

Entities selectable as a project's status source, grouped for a picker:
`[{"Status lists", [{name, uuid}]}, {"Other entities", [{name, uuid}]}]`.
Status-tagged catalogs (the shared entity + per-project opt-ins, marked
`settings["source"] = "phoenix_kit_projects"`) come first; every other
entity follows, since any entity's records can serve as statuses (record
title = label). Empty groups are omitted. `[]` when entities is
unavailable.

# `lock_status_source`

```elixir
@spec lock_status_source(map(), PhoenixKitProjects.Schemas.Project.t()) :: map()
```

Server-side mate to the edit form's locked status-source picker: a started
project's statuses were cemented at `started_at` and are frozen, so this
drops any `status_entity_uuid` from incoming update attrs once `started?/1`.
Unstarted projects and templates pass through unchanged (the source is still
a live, pre-start choice). Handles string- and atom-keyed attrs.

# `per_project_entity_name`

```elixir
@spec per_project_entity_name(PhoenixKitProjects.Schemas.Project.t()) :: String.t()
```

Per-project custom status entity name: `project_status_<32 hex>` (the
full project UUID with hyphens stripped — 47 chars, under the 50-char
entity-name limit, and collision-free unlike a UUIDv7 prefix).

# `recement_project_statuses`

```elixir
@spec recement_project_statuses(PhoenixKitProjects.Schemas.Project.t()) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
```

Clear + re-copy the chosen catalog into local rows, atomically. Used when
a started project switches its status source (from the show-page picker).
Existing local edits are intentionally discarded — the user chose a
different list. Returns the (possibly slug-reconciled) project, or
`{:error, changeset}` if the reconciling write fails. No-op (returns
`{:ok, project}`) when entities is unavailable.

# `remove_project_status`

```elixir
@spec remove_project_status(PhoenixKitProjects.Schemas.ProjectStatus.t()) ::
  {:ok, PhoenixKitProjects.Schemas.ProjectStatus.t()}
  | {:error, Ecto.Changeset.t()}
```

Deletes a cemented status row. If the deleted row was the project's
currently-selected status, `current_status_slug` is cleared too (and a
`:project_status_changed` broadcast fires) so the selection never dangles
at a slug with no matching row. Slugs are unique per project, so deleting
the row removes the only match.

# `resolve_catalog_entity_uuid`

```elixir
@spec resolve_catalog_entity_uuid(PhoenixKitProjects.Schemas.Project.t()) ::
  {:ok, String.t()} | {:error, term()}
```

Resolves which catalog entity a project/template draws from: its own
`status_entity_uuid` (per-project choice), else the **admin-chosen global
default** (the `projects_default_status_entity_uuid` setting, picked on the
projects Settings page). Returns `{:ok, uuid}` or `{:error, :no_status_entity}`
when neither is set — nothing is auto-provisioned.

# `reverse_reference_count`

```elixir
@spec reverse_reference_count(String.t()) :: non_neg_integer()
```

Counts projects/templates currently sourcing their status list from the
given catalog entity. This is the callback a host registers via
`config :phoenix_kit_entities, reverse_references: [{"project_status",
&PhoenixKitProjects.Statuses.reverse_reference_count/1}]` to power the
entities admin's "Used by N" hint. Started projects no longer reference
the catalog (they're cemented), which is the intended semantics.

# `set_current_status`

```elixir
@spec set_current_status(
  PhoenixKitProjects.Schemas.Project.t(),
  String.t() | nil,
  keyword()
) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
```

Sets a project's current workflow status by slug (or clears it with
`nil`). Validates the slug against the project's resolved status list
before writing. Delegates the write to
`PhoenixKitProjects.Projects.set_current_status_slug/2` so the single
PubSub broadcast fires. Returns `{:ok, project}` or `{:error, reason}`.

# `set_default_status_entity`

```elixir
@spec set_default_status_entity(String.t() | nil) :: term()
```

Sets (or clears with `nil`) the global default status entity.

# `set_status_entity`

```elixir
@spec set_status_entity(
  PhoenixKitProjects.Schemas.Project.t(),
  String.t() | nil,
  keyword()
) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
```

Points a project at `entity_uuid` as its status source (nil = the shared
default). For an **already-started** project this re-cements immediately
— the chosen entity's statuses are snapshotted into fresh local rows
(replacing any existing cemented rows), per the "cement on selection"
rule — so existing/running projects get a usable, frozen status set.
Unstarted projects just record the choice and cement at start as usual.

# `shared_catalog_statuses`

```elixir
@spec shared_catalog_statuses() :: [status()]
```

Statuses from the shared catalog entity if it already exists — does
NOT provision it (unlike `resolve_catalog_entity_uuid/1`). Used by the
list view's status filter, which shouldn't seed the entity as a side
effect of rendering. `[]` when the shared entity hasn't been created
yet or entities is unavailable.

# `started?`

```elixir
@spec started?(PhoenixKitProjects.Schemas.Project.t()) :: boolean()
```

True once a project has started (has a `started_at`).

# `statuses_for`

```elixir
@spec statuses_for(PhoenixKitProjects.Schemas.Project.t()) :: [status()]
```

The status list for a project — cemented local rows once it has started,
otherwise the live catalog list it draws from. `[]` when entities is
unavailable and the project hasn't started.

# `statuses_for_projects`

```elixir
@spec statuses_for_projects([PhoenixKitProjects.Schemas.Project.t()]) :: %{
  optional(String.t()) =&gt; status() | nil
}
```

Batched current-status lookup for a list of projects. Returns
`%{project_uuid => status() | nil}`. Groups started projects by a single
local query and unstarted projects by their resolved catalog entity (one
`list_by_entity` per distinct entity) to avoid an N+1. Empty map when
entities is unavailable.

# `update_project_status_row`

```elixir
@spec update_project_status_row(PhoenixKitProjects.Schemas.ProjectStatus.t(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.ProjectStatus.t()}
  | {:error, Ecto.Changeset.t()}
```

Updates a cemented status row.

# `update_project_with_statuses`

```elixir
@spec update_project_with_statuses(PhoenixKitProjects.Schemas.Project.t(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
```

Updates a project (arbitrary form attrs) and, when a **started** project's
status source changed, re-cements its local rows — all in one transaction.
This is the atomic edit-form entry point: `update_project/2` followed by a
separate re-cement would leave a failure window where the project points at
a new entity with stale local rows. Returns the (possibly slug-reconciled)
project. Unstarted projects just record the choice and cement at start.

# `use_status_translations?`

```elixir
@spec use_status_translations?(PhoenixKitProjects.Schemas.Project.t()) :: boolean()
```

Whether status titles display in the viewer's content locale for this
project: the per-project override if set, else the global
`projects_use_status_translations` setting (default `true`). Translations
are always captured; this only gates display.

# `use_status_translations?`

```elixir
@spec use_status_translations?(PhoenixKitProjects.Schemas.Project.t(), boolean()) ::
  boolean()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
