# `PhoenixKitCatalogue.Web.Helpers`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/0.1.14/lib/phoenix_kit_catalogue/web/helpers.ex#L1)

Tiny utilities shared by every catalogue LiveView. Imported into LVs
via the standard `import PhoenixKitCatalogue.Web.Helpers` line.

Currently exports:

  * `actor_opts/1` — extract the current user's UUID from socket
    assigns, return `[actor_uuid: uuid]` for the `opts \\ []` keyword
    list every mutating context function accepts. Returns `[]` when
    no user is signed in (e.g. inside a test that mounts the LV with
    a bare conn). The atom is suitable to thread through
    `Catalogue.create_*` / `update_*` / `trash_*` / `restore_*` /
    `permanently_delete_*` etc.
  * `actor_uuid/1` — the raw UUID (or `nil`). Use when you need the
    value directly rather than a keyword list, e.g. when building
    activity-log metadata in a LiveView.
  * `log_operation_error/3` — engineer-visible `Logger.error` for a
    failed mutation **plus** an Activity row tagged
    `db_pending: true` so the user-visible audit feed records the
    attempted action even when it fails. See
    `PhoenixKitCatalogue.Catalogue.ActivityLog` for the
    success-vs-failure layering.

# `actor_opts`

```elixir
@type actor_opts() :: [{:actor_uuid, Ecto.UUID.t()}] | []
```

Convenience alias for the keyword list shape mutating ctx fns accept.

# `actor_opts`

```elixir
@spec actor_opts(Phoenix.LiveView.Socket.t()) :: actor_opts()
```

Extracts `[actor_uuid: uuid]` from `socket.assigns.phoenix_kit_current_user`.

Returns `[]` when no user is signed in. Pass the result straight into
any `PhoenixKitCatalogue.Catalogue` mutating function as its trailing
`opts` argument.

# `actor_uuid`

```elixir
@spec actor_uuid(Phoenix.LiveView.Socket.t()) :: Ecto.UUID.t() | nil
```

Returns the current user's UUID from socket assigns, or `nil`.

# `derive_activity_action`

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

Maps an LV operation string + entity_type to the canonical activity
action atom the catalogue context already uses on the success path.

Falls back to `nil` when the operation doesn't follow the
`<verb>_<entity>` shape; the caller skips the audit-row write in
that case (engineer log still fires).

# `log_operation_error`

```elixir
@spec log_operation_error(Phoenix.LiveView.Socket.t(), String.t(), map()) :: :ok
```

Logs a failed LV mutation in two places at once:

1. **Engineer log** — `Logger.error` with the operation, the
   LV-level entity context, and the changeset / atom reason. This
   is the rich-context line that production-incident triage reads.
2. **User-visible audit row** — an Activity entry with the same
   action atom the success path would have written, plus
   `metadata.db_pending: true`. The audit feed therefore records
   **what the user attempted**, not just **what succeeded** — a
   deliberate change in the post-Apr 2026 pipeline (workspace
   `AGENTS.md` C12 agent #2 — "Activity logging coverage").

The action atom is derived from `operation` via
`derive_activity_action/2`. Validation cycles (form-validate
events) never reach this helper — by construction it's only called
from `{:error, _}` handle_event branches, where the failure is a
real infrastructure / consistency error worth auditing.

## Expected `context` keys

  * `:entity_type` — `"item"` / `"category"` / `"catalogue"` /
    `"manufacturer"` / `"supplier"` (drives both the activity
    `resource_type` and the action-atom prefix).
  * `:entity_uuid` — primary-key UUID; lands as `resource_uuid`.
  * `:reason` — an `%Ecto.Changeset{}`, an atom, or any other
    `inspect`able shape. Logged engineer-side; on the audit row
    it's summarised into PII-safe `metadata.error_keys` (changeset
    field names only — never values, since user-typed strings can
    carry PII).

Activity-log failures (missing table, ownership errors, sandbox
exit) are swallowed by `ActivityLog.log/1`; they never bubble up
to the LV.

# `status_label`

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

Translates a catalogue/category/item/manufacturer/supplier `status`
field value to a localised label via gettext.

Handles every status string that any catalogue schema can emit
(`active` / `inactive` / `archived` / `deleted` / `discontinued`)
with explicit literal `gettext(...)` clauses so `mix gettext.extract`
picks them up. Unknown status values pass through unchanged — never
use `String.capitalize/1` on translated text because the result
would pin English casing on a value the extractor can't see.

---

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