{"id":47711650,"url":"https://github.com/beamlabeu/phoenix_kit_document_creator","last_synced_at":"2026-05-02T10:03:18.337Z","repository":{"id":346382338,"uuid":"1187558082","full_name":"BeamLabEU/phoenix_kit_document_creator","owner":"BeamLabEU","description":"Document Creator module for PhoenixKit — uses Google Docs and Google Drive to use templates, and creates documents with users data","archived":false,"fork":false,"pushed_at":"2026-04-30T00:43:15.000Z","size":930,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-30T02:19:38.402Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BeamLabEU.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-20T21:38:16.000Z","updated_at":"2026-04-30T00:43:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/BeamLabEU/phoenix_kit_document_creator","commit_stats":null,"previous_names":["beamlabeu/phoenix_kit_document_creator"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/BeamLabEU/phoenix_kit_document_creator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BeamLabEU%2Fphoenix_kit_document_creator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BeamLabEU%2Fphoenix_kit_document_creator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BeamLabEU%2Fphoenix_kit_document_creator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BeamLabEU%2Fphoenix_kit_document_creator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BeamLabEU","download_url":"https://codeload.github.com/BeamLabEU/phoenix_kit_document_creator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BeamLabEU%2Fphoenix_kit_document_creator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32530178,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T01:12:54.858Z","status":"online","status_checked_at":"2026-05-02T02:00:05.923Z","response_time":132,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-02T18:35:26.389Z","updated_at":"2026-05-02T10:03:18.330Z","avatar_url":"https://github.com/BeamLabEU.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Document Creator\n\nA [PhoenixKit](https://hex.pm/packages/phoenix_kit) module for document template management and PDF generation via Google Docs. Templates and documents live in Google Drive as Google Docs. Variables use `{{ placeholder }}` syntax and are substituted via the Google Docs API. PDF export uses the Google Drive export endpoint.\n\n## Features\n\n- **Google Docs as editor** — create and edit templates/documents directly in Google Docs\n- **Template variables** — `{{ client_name }}`, `{{ date }}` placeholders auto-detected from document text\n- **Document creation from templates** — copy a template, fill in variables via API, get a ready document\n- **PDF export** — via Google Drive API (no local Chrome/binary dependency)\n- **Thumbnail previews** — fetched from Google Drive API for listing cards\n- **OAuth 2.0 integration** — connect a Google account in Admin \u003e Settings\n\n## Setup\n\n### 1. Add the dependency\n\n```elixir\n# In parent app's mix.exs\n{:phoenix_kit_document_creator, \"~\u003e 0.2\"}\n```\n\n### 2. Enable the module\n\nStart your app, go to **Admin \u003e Modules**, enable **Document Creator**.\n\n### 3. Connect Google Docs\n\n1. Create a Google Cloud project with **Docs API** and **Drive API** enabled\n2. Create an **OAuth 2.0 Client ID** (Web application type)\n3. Go to **Admin \u003e Settings \u003e Document Creator**\n4. Enter the Client ID and Client Secret\n5. Click **Connect** and authorize the Google account\n6. Create `/templates` and `/documents` folders in the connected Google Drive\n\n### Prerequisites\n\n- **Google Cloud project** — with Docs API and Drive API enabled\n- **PhoenixKit \u003e= 1.7** — provides the Module behaviour, Settings API, and admin layout\n\n## Architecture\n\n### How it works\n\n```\nTemplate (Google Doc with {{ variables }})\n    |  copy via Drive API\n    v\nDocument (Google Doc copy)\n    |  replaceAllText via Docs API\n    v\nDocument (variables filled in)\n    |  export via Drive API\n    v\nPDF\n```\n\n1. **Templates** are Google Docs stored in a `/templates` folder in Drive. Variables like `{{ client_name }}` are auto-detected from the document text.\n2. **Documents** are created by copying a template via the Drive API, then substituting variable values using the Docs API `replaceAllText` endpoint.\n3. **PDF export** uses the Drive API file export endpoint — no local Chrome or binary dependencies needed.\n\n### Google Drive as source of truth\n\nAll document content lives in Google Drive. The Phoenix app serves as a coordinator:\n- Manages OAuth credentials (stored in PhoenixKit Settings)\n- Lists files from Drive folders\n- Handles variable detection and substitution via API\n- Exports PDFs via Drive API\n- Displays thumbnails fetched from Drive\n\n### Dependencies\n\n| Package | Purpose |\n|---|---|\n| `phoenix_kit` | Module behaviour, Settings API, admin layout, Routes |\n| `phoenix_live_view` | Admin pages |\n| `req` | HTTP client for Google Docs/Drive API |\n\n## Admin Pages\n\nThe module registers 3 admin tabs plus a settings tab:\n\n| Page | Path | Description |\n|---|---|---|\n| **Document Creator** | `/admin/document-creator` | Landing page (redirects to first subtab) |\n| **Documents** | `/admin/document-creator/documents` | List documents from Drive with thumbnails |\n| **Templates** | `/admin/document-creator/templates` | List templates from Drive with thumbnails |\n\n**Settings:**\n\n| Page | Path | Description |\n|---|---|---|\n| **Google OAuth** | `/admin/settings/document-creator` | Connect/disconnect Google account, manage OAuth credentials |\n\n## Database Schema\n\nTables are created by PhoenixKit core's migration system (V86 for the initial\ntables, V94 for the `google_doc_id` / `status` / `path` / `folder_id` columns):\n\n### `phoenix_kit_doc_templates`\n\n| Column | Type | Description |\n|---|---|---|\n| `uuid` | UUID (PK) | Auto-generated (UUIDv7) |\n| `name` | string | Template name |\n| `slug` | string | URL-safe identifier (unique, auto-generated from name) |\n| `description` | text | Template description |\n| `status` | string | `\"published\"`, `\"trashed\"`, `\"lost\"`, or `\"unfiled\"` |\n| `google_doc_id` | string | Google Doc ID (partial unique index) |\n| `path` | string | Human-readable Drive path (e.g. `\"documents/order-1/sub-4\"`) |\n| `folder_id` | string | Drive folder ID of the file's current parent |\n| `variables` | jsonb | Array of variable definitions detected in the template |\n| `config` | jsonb | Configuration (e.g., paper_size) |\n| `thumbnail` | text | Base64 data URI for preview |\n| `content_html` / `content_css` / `content_native` | mixed | Legacy columns — no longer populated |\n\n### `phoenix_kit_doc_documents`\n\n| Column | Type | Description |\n|---|---|---|\n| `uuid` | UUID (PK) | Auto-generated (UUIDv7) |\n| `name` | string | Document name |\n| `status` | string | `\"published\"`, `\"trashed\"`, `\"lost\"`, or `\"unfiled\"` |\n| `google_doc_id` | string | Google Doc ID (partial unique index) |\n| `path` | string | Human-readable Drive path |\n| `folder_id` | string | Drive folder ID of the file's current parent |\n| `template_uuid` | UUID (FK) | Template this was created from (optional) |\n| `variable_values` | jsonb | Map of variable values used during creation |\n| `config` | jsonb | Configuration |\n| `thumbnail` | text | Base64 data URI for preview |\n| `created_by_uuid` | UUID | Optional FK to users |\n| `content_html` / `content_css` / header/footer cols | mixed | Legacy columns — no longer populated |\n\n### `phoenix_kit_doc_headers_footers`\n\nLegacy table — headers and footers are now handled natively by Google Docs.\nRetained for migration compatibility; a future migration will drop it.\n\n## Context API\n\nThe module exposes three layers. See `AGENTS.md` for the full breakdown; the\nquick reference below covers the most common calls.\n\n### `PhoenixKitDocumentCreator.Documents` — combined Drive + DB\n\nReads go to the local DB (fast); writes go to Drive first then DB. Mutating\nfunctions accept `opts` with `:actor_uuid` for activity logging.\n\n```elixir\n# Listing (DB-only, no Drive round-trip)\nDocuments.list_templates_from_db()\nDocuments.list_documents_from_db()\nDocuments.list_trashed_templates_from_db()\nDocuments.list_trashed_documents_from_db()\n\n# Sync (recursive walker — picks up files nested in subfolders too)\nDocuments.sync_from_drive()\n\n# Create\nDocuments.create_template(\"Invoice Template\", actor_uuid: uid)\nDocuments.create_document(\"Blank Doc\", actor_uuid: uid)\n\n# Create a document from a template, optionally into a subfolder you manage\nDocuments.create_document_from_template(template_file_id, %{\"client\" =\u003e \"Acme\"},\n  name: \"Acme Contract\",\n  parent_folder_id: sub_folder_id,   # optional — defaults to managed documents root\n  path: \"documents/order-1/sub-4\",   # optional — human-readable path\n  actor_uuid: uid\n)\n\n# Register a Drive file your own code created (no Drive calls — DB-only upsert)\nDocuments.register_existing_document(%{\n  google_doc_id: doc_id,\n  name: \"Invoice\",\n  template_uuid: tpl_uuid,\n  variable_values: vars,\n  folder_id: sub_folder_id,\n  path: \"documents/order-1/sub-4\"\n}, actor_uuid: uid)\n\nDocuments.register_existing_template(%{google_doc_id: gid, name: \"Tpl\"})\n\n# Delete (soft — moves to the deleted folder)\nDocuments.delete_document(file_id, actor_uuid: uid)\nDocuments.delete_template(file_id, actor_uuid: uid)\nDocuments.restore_document(file_id, actor_uuid: uid)\nDocuments.restore_template(file_id, actor_uuid: uid)\n\n# Unfiled resolution (file found outside the managed tree)\nDocuments.move_to_templates(file_id, actor_uuid: uid)\nDocuments.move_to_documents(file_id, actor_uuid: uid)\nDocuments.set_correct_location(file_id, actor_uuid: uid)\n\n# Variable detection on a template\nDocuments.detect_variables(file_id)           # {:ok, [\"client_name\", \"date\"]}\n\n# PDF export\nDocuments.export_pdf(file_id, name: \"Acme Contract\", actor_uuid: uid)\n\n# Folder helpers (cached via Settings, lazy-discovered on first access)\nDocuments.get_folder_ids()\nDocuments.refresh_folders()\nDocuments.templates_folder_url()\nDocuments.documents_folder_url()\n\n# PubSub — broadcast {:files_changed, self()} on \"document_creator:files\"\n# topic. Bulk callers passing `emit_pubsub: false` to the register functions\n# should call this once at the end to resync connected admin LiveViews.\nDocuments.broadcast_files_changed()\n```\n\n### `PhoenixKitDocumentCreator.Errors` — error atom dispatcher\n\nContext and client functions return `{:error, :atom}` tuples — never raw\nstrings. Translate at the UI / API boundary via `Errors.message/1`, which\nreturns gettext-wrapped strings (translations live in core `phoenix_kit`):\n\n```elixir\ncase Documents.create_template(\"Invoice\", actor_uuid: uid) do\n  {:ok, template} -\u003e ...\n  {:error, reason} -\u003e\n    flash = PhoenixKitDocumentCreator.Errors.message(reason)\n    put_flash(socket, :error, flash)\nend\n```\n\nAtoms that flow out of the public API: `:templates_folder_not_found`,\n`:documents_folder_not_found`, `:invalid_parent_folder_id`,\n`:invalid_google_doc_id`, `:missing_google_doc_id`, `:missing_name`,\n`:not_found`, `:file_trashed`, `:invalid_file_id`, `:no_doc_id`,\n`:no_thumbnail`, `:create_document_failed`, `:create_folder_failed`,\n`:folder_search_failed`, `:move_failed`, `:copy_failed`,\n`:pdf_export_failed`, `:thumbnail_link_failed`, `:thumbnail_fetch_failed`,\n`:list_files_failed`, `:get_file_parents_failed`, `:sync_failed`,\n`:max_depth_exceeded`. Every atom has a literal `gettext(\"…\")` clause in\n`Errors.message/1`; unknown atoms fall through to `inspect/1`.\n\n### `PhoenixKitDocumentCreator.GoogleDocsClient` — direct Drive + Docs API\n\nOAuth credentials and token refresh live in `PhoenixKit.Integrations` under\nthe `\"google\"` provider; this module delegates authentication there.\n\nThe active connection is stored as the integration row's **uuid** in\n`document_creator_settings.google_connection`. `active_integration_uuid/0`\nis the read accessor; `migrate_legacy/0` (the `PhoenixKit.Module` boot\ncallback) handles two kinds of pre-uuid data on upgrade:\n\n1. The legacy `document_creator_google_oauth` settings key with locally-\n   stored OAuth tokens, migrated into a real `PhoenixKit.Integrations`\n   row under `\"google:default\"`. After a successful migration the\n   plaintext secrets in the legacy key are wiped (the row is reset to\n   `%{}`) so they don't survive the move to encrypted Integrations\n   storage.\n2. Name-string `google_connection` references (`\"google\"` /\n   `\"google:my-name\"`) from before the uuid switch, rewritten in place\n   to the matching row's uuid.\n\nLazy on-read promotion in `active_integration_uuid/0` covers any\nrecord the boot pass missed — first request after upgrade rewrites the\nsetting transparently. Host apps trigger boot migration via\n`PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0` from\n`Application.start/2`; if you never call it, the lazy path keeps things\nworking at the cost of one extra Settings round-trip per affected\nrequest.\n\n```elixir\nGoogleDocsClient.connection_status()             # {:ok, %{email: ...}} | {:error, reason}\nGoogleDocsClient.get_credentials()               # {:ok, creds} | {:error, :not_configured}\n\n# Folders\nGoogleDocsClient.find_folder_by_name(name, opts)\nGoogleDocsClient.create_folder(name, opts)\nGoogleDocsClient.find_or_create_folder(name, opts)\nGoogleDocsClient.ensure_folder_path(\"a/b/c\", opts)\nGoogleDocsClient.discover_folders()              # resolves all four managed folders\nGoogleDocsClient.list_subfolders(parent_id)      # paginated, alphabetical\nGoogleDocsClient.list_folder_files(folder_id)    # paginated; direct children only\nGoogleDocsClient.get_folder_url(folder_id)\n\n# Files\nGoogleDocsClient.create_document(title, parent: folder_id)\nGoogleDocsClient.copy_file(src_id, new_name, parent: folder_id)\nGoogleDocsClient.move_file(file_id, to_folder_id)\nGoogleDocsClient.export_pdf(file_id)             # {:ok, pdf_binary}\nGoogleDocsClient.fetch_thumbnail(file_id)        # {:ok, \"data:image/png;base64,...\"}\nGoogleDocsClient.file_status(file_id)            # {:ok, %{trashed: bool, parents: [id]}}\nGoogleDocsClient.file_location(file_id)          # {:ok, %{folder_id, path, trashed}}\nGoogleDocsClient.get_edit_url(file_id)\n\n# Docs\nGoogleDocsClient.get_document(doc_id)\nGoogleDocsClient.get_document_text(doc_id)\nGoogleDocsClient.batch_update(doc_id, requests)\nGoogleDocsClient.replace_all_text(doc_id, %{\"var\" =\u003e \"value\"})\n```\n\n### `PhoenixKitDocumentCreator.GoogleDocsClient.DriveWalker` — paginated + recursive traversal\n\nCanonical paginated listing primitive. `list_folder_files/1` and\n`list_subfolders/1` on the parent client delegate here.\n\n```elixir\nalias PhoenixKitDocumentCreator.GoogleDocsClient.DriveWalker\n\nDriveWalker.list_files(folder_id)                # {:ok, [file_map]} — paginated\nDriveWalker.list_folders(folder_id)              # {:ok, [folder_map]} — paginated, alphabetical\n\n# BFS the whole tree — returns every descendant folder and every Google Doc\n# in any of them, each file annotated with its owning `folder_id` and the\n# resolved human-readable `path`.\nDriveWalker.walk_tree(root_folder_id,\n  root_path: \"documents\",   # caller-supplied path to anchor descendants at\n  max_depth: 20             # defensive cap; root is depth 0\n)\n```\n\n## Variable System\n\nTemplates support `{{ variable_name }}` placeholders.\n\n### Auto-detection\n\nVariables are extracted from Google Doc text content via regex:\n\n```elixir\nPhoenixKitDocumentCreator.Variable.extract_variables(\"Dear {{ client_name }},\")\n# =\u003e [\"client_name\"]\n```\n\n### Type guessing\n\nVariable types are guessed from names:\n\n| Name contains | Guessed type |\n|---|---|\n| `date` | `:date` |\n| `amount`, `price` | `:currency` |\n| `description`, `notes` | `:multiline` |\n| anything else | `:text` |\n\n## Navigation (Paths Module)\n\nAll paths go through `PhoenixKit.Utils.Routes.path/1` via the centralized `Paths` module:\n\n```elixir\nalias PhoenixKitDocumentCreator.Paths\n\nPaths.index()                  # /admin/document-creator\nPaths.templates()              # /admin/document-creator/templates\nPaths.documents()              # /admin/document-creator/documents\nPaths.settings()               # /admin/settings/document-creator\n```\n\n## PhoenixKit Module Integration\n\n### Behaviour callbacks\n\n| Callback | Value |\n|---|---|\n| `module_key` | `\"document_creator\"` |\n| `module_name` | `\"Document Creator\"` |\n| `version` | `\"0.1.2\"` |\n| `permission_metadata` | Key: `\"document_creator\"`, icon: `\"hero-document-text\"` |\n| `children` | `[]` |\n| `admin_tabs` | 3 tabs (parent + documents + templates) |\n| `settings_tabs` | 1 tab (Google OAuth settings) |\n\n### Permission\n\nThe module registers `\"document_creator\"` as a permission key. Owner and Admin roles get access automatically. Custom roles must be granted access via Admin \u003e Roles.\n\n## Project Structure\n\n```\nlib/\n  phoenix_kit_document_creator.ex              # Main module (behaviour callbacks, tab registration)\n  phoenix_kit_document_creator/\n    documents.ex                               # Context: combined Drive + DB operations\n    google_docs_client.ex                      # Google Docs + Drive API client (OAuth via Integrations)\n    google_docs_client/\n      drive_walker.ex                          # Paginated + recursive Drive traversal\n    variable.ex                                # Extract {{ variables }} and guess types\n    paths.ex                                   # Centralized URL path helpers\n    schemas/\n      document.ex                              # Document schema\n      header_footer.ex                         # Header/footer schema — legacy, deprecated\n      template.ex                              # Template schema (with slug auto-gen)\n    web/\n      documents_live.ex                        # Landing page (templates + documents tabs)\n      google_oauth_settings_live.ex            # OAuth settings page\n      components/\n        create_document_modal.ex               # Multi-step creation modal\n```\n\n## Development\n\n### Code quality\n\n```bash\nmix format\nmix credo --strict\nmix dialyzer\n```\n\n### Running tests\n\n```bash\n# Unit tests only (no database needed)\nmix test --exclude integration\n\n# All tests (requires PostgreSQL)\ncreatedb phoenix_kit_document_creator_test\nmix test\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeamlabeu%2Fphoenix_kit_document_creator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeamlabeu%2Fphoenix_kit_document_creator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeamlabeu%2Fphoenix_kit_document_creator/lists"}