# `PhoenixKitDocumentCreator.Documents`
[🔗](https://github.com/BeamLabEU/phoenix_kit_document_creator/blob/0.2.8/lib/phoenix_kit_document_creator/documents.ex#L1)

Context module for managing templates and documents via Google Drive.

Google Drive is the single source of truth for file content. This module
mirrors file metadata (name, google_doc_id, status, thumbnails, variables)
to the local database for fast listing and audit tracking.

## API layers

This module provides **combined** operations (Drive + DB). For direct access:

- **Drive-only** — Use `PhoenixKitDocumentCreator.GoogleDocsClient` for raw Google
  Drive/Docs API calls (create files, list folders, export PDF, move files) without
  touching the local database.
- **DB-only** — Use `list_templates_from_db/0`, `list_documents_from_db/0`,
  `load_cached_thumbnails/1`, `persist_thumbnail/2` for local DB queries.
- **Combined** — Use `create_template/2`, `create_document/2`, `sync_from_drive/0`,
  `delete_template/2`, etc. which coordinate between Drive and DB.

# `broadcast_files_changed`

```elixir
@spec broadcast_files_changed() :: :ok
```

Broadcast `{:files_changed, self()}` on the `document_creator:files` topic.

Use this after a bulk `register_existing_document/2` / `register_existing_template/2`
call that passed `emit_pubsub: false`, to trigger a single resync in any
connected admin LiveViews.

Silently no-ops if the PubSub system isn't available (e.g. background
jobs or tests without a running PubSub registry).

# `create_document`

```elixir
@spec create_document(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Create a blank document in the documents folder. Returns `{:ok, %{doc_id, name, url}}`.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `create_document_from_template`

```elixir
@spec create_document_from_template(String.t(), map(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Create a document from a template by copying and filling variables.

1. Copies the template Google Doc into the target folder
2. Replaces all `{{variable}}` placeholders with values
3. Persists the document record with variable_values and template link
4. Returns `{:ok, %{doc_id, url}}`

## Options

- `:name` — document name (default `"New Document"`)
- `:actor_uuid` — UUID of the user performing the action (activity log)
- `:parent_folder_id` — Drive folder ID to copy into. Defaults to the
  managed documents folder. Supply this to place the new document in a
  subfolder (e.g. `order-123/sub-4/`) you manage yourself.
- `:path` — human-readable path string to store on the record. Only
  meaningful when `:parent_folder_id` is also supplied. If omitted when
  `:parent_folder_id` is given, the stored `path` is left unset and the
  next `sync_from_drive/0` fills it from the walker.

# `create_template`

```elixir
@spec create_template(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Create a blank template in the templates folder. Returns `{:ok, %{doc_id, name, url}}`.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `delete_document`

```elixir
@spec delete_document(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Move a document to the deleted/documents folder.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `delete_template`

```elixir
@spec delete_template(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Move a template to the deleted/templates folder.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `detect_variables`

```elixir
@spec detect_variables(String.t()) :: {:ok, [String.t()]} | {:error, term()}
```

Detect `{{ variables }}` in a Google Doc's text content.

# `documents_folder_url`

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

Get the Google Drive URL for the documents folder.

# `export_pdf`

```elixir
@spec export_pdf(
  String.t(),
  keyword()
) :: {:ok, binary()} | {:error, term()}
```

Export a Google Doc to PDF. Returns `{:ok, pdf_binary}`.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)
- `:name` — document name (for activity metadata)

# `fetch_thumbnails_async`

```elixir
@spec fetch_thumbnails_async([map()], pid()) :: :ok
```

Fetch thumbnails for a list of Drive files asynchronously.

Spawns a single supervised parent task under `PhoenixKit.TaskSupervisor`
that fans out via `Task.async_stream/3` with a bounded `max_concurrency`
so opening a folder with hundreds of files doesn't fire hundreds of
simultaneous Drive requests. Each completion sends `{:thumbnail_result,
file_id, data_uri}` back to `caller_pid` and persists the thumbnail to
the DB. The parent is `restart: :temporary` so it dies cleanly if the
caller LV closes mid-fetch — but in-flight persists still complete.

# `get_folder_ids`

```elixir
@spec get_folder_ids() :: map()
```

Get the folder IDs (auto-discovers if not cached).

# `list_documents_from_db`

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

List documents from the local DB. Returns maps compatible with the LiveView.

# `list_templates_from_db`

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

List templates from the local DB. Returns maps compatible with the LiveView.

# `list_trashed_documents_from_db`

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

List trashed documents from the local DB.

# `list_trashed_templates_from_db`

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

List trashed templates from the local DB.

# `load_cached_thumbnails`

```elixir
@spec load_cached_thumbnails([String.t()] | any()) :: %{
  required(String.t()) =&gt; String.t()
}
```

Load cached thumbnails from DB for a list of google_doc_ids.

# `log_manual_action`

```elixir
@spec log_manual_action(
  String.t(),
  keyword()
) :: :ok
```

Log a manual user action to the activity feed.

# `move_to_documents`

```elixir
@spec move_to_documents(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Move a file into the managed documents folder and classify it as a document.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `move_to_templates`

```elixir
@spec move_to_templates(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Move a file into the managed templates folder and classify it as a template.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `persist_thumbnail`

```elixir
@spec persist_thumbnail(String.t(), String.t()) :: :ok
```

Persist a thumbnail data URI to the DB by google_doc_id.

# `pubsub_topic`

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

The PubSub topic on which `{:files_changed, self()}` messages are
broadcast whenever a template or document DB record is mutated.

Admin LiveViews subscribe to this topic in `mount/3`. Prefer calling
this helper over hard-coding the topic string so the two stay in sync.

# `refresh_folders`

```elixir
@spec refresh_folders() :: map()
```

Re-discover folder IDs from Drive.

# `register_existing_document`

```elixir
@spec register_existing_document(
  map(),
  keyword()
) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()}
  | {:error, Ecto.Changeset.t() | term()}
```

Register a Drive document that the caller has already created (or already
knows about) into the local DB.

Use this when your own wrapper code handles the Drive-side work (copy,
placement in a subfolder, variable substitution) and you just need the
file to appear in `list_documents_from_db/0` and be classified correctly
by future `sync_from_drive/0` runs.

**Makes no Drive API calls.** This is a pure DB upsert — garbage inputs
do not error, they self-correct on the next sync (the walker rewrites
`path`/`folder_id`, and files that are not actually in the managed tree
get classified `:unfiled` or `:lost` per the usual reconciliation rules).

## `attrs` — map keyed by atoms or strings

| Key               | Required | Notes                                                     |
| ----------------- | -------- | --------------------------------------------------------- |
| `google_doc_id`   | yes      | Drive file ID                                             |
| `name`            | yes      | Display name                                              |
| `template_uuid`   | no       | UUID of the source template, if applicable                |
| `variable_values` | no       | Map of variables substituted during generation            |
| `folder_id`       | no       | Actual Drive folder holding the file. Defaults to managed |
| `path`            | no       | Human-readable path. Defaults to managed root path        |
| `status`          | no       | Defaults to `"published"`                                 |
| `thumbnail`       | no       | Optional data URI                                         |

If `:folder_id` points outside the managed documents tree, the next
`sync_from_drive/0` will classify the record as `:unfiled` and surface
the resolution popup in the admin UI.

## `opts`

- `:actor_uuid` — user UUID for the activity log
- `:emit_pubsub` — default `true`. Broadcasts `:files_changed` on the
  `document_creator:files` topic so connected admin LiveViews re-sync.
  Bulk callers (e.g. a backfill script registering hundreds of rows)
  should pass `false` and trigger **one** broadcast or sync at the end:

      Enum.each(rows, &Documents.register_existing_document(&1, emit_pubsub: false))
      Documents.broadcast_files_changed()

# `register_existing_template`

```elixir
@spec register_existing_template(
  map(),
  keyword()
) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()}
  | {:error, Ecto.Changeset.t() | term()}
```

Register a Drive template that the caller has already created or knows about.

Symmetric to `register_existing_document/2` — see its documentation for the
`attrs` shape and options. Unlike documents, template registration does not
accept `template_uuid` or `variable_values`.

# `restore_document`

```elixir
@spec restore_document(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Restore a trashed document back to the documents folder.

# `restore_template`

```elixir
@spec restore_template(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Restore a trashed template back to the templates folder.

# `set_correct_location`

```elixir
@spec set_correct_location(
  String.t(),
  keyword()
) :: :ok | {:error, term()}
```

Persist the file's current parent folder as its accepted location.

## Options

- `:actor_uuid` — UUID of the user performing the action (for activity logging)

# `sync_from_drive`

```elixir
@spec sync_from_drive() :: :ok | {:error, :sync_failed}
```

Sync local DB with Google Drive.

Recursively walks both managed trees (templates and documents), upserts every
Google Doc found (including those nested in subfolders) with its actual
parent `folder_id` and human-readable `path`, then reconciles DB records
against the walk — records whose `google_doc_id` is missing from the walk
are re-classified via a per-file Drive API call as `trashed`, `lost`, or
`unfiled` according to their current Drive parents.

Files in any descendant of a managed folder are treated as `published`.

# `templates_folder_url`

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

Get the Google Drive URL for the templates folder.

# `upsert_document_from_drive`

```elixir
@spec upsert_document_from_drive(map(), map()) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()}
  | {:error, Ecto.Changeset.t()}
```

Upsert a document record from a Google Drive file map.

# `upsert_template_from_drive`

```elixir
@spec upsert_template_from_drive(map(), map()) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()}
  | {:error, Ecto.Changeset.t()}
```

Upsert a template record from a Google Drive file map.

---

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