# Document Templates — admin-managed forms (feature) A from-scratch subsystem (2026-06-14): admins upload `.docx` templates with Jinja `{{ placeholders }}`; applicants fill a form **generated from each template's fields** and download a server-rendered PDF. Independent of the hardcoded *sáng kiến* form pipeline (which stays as-is). ## Data + storage - Table **`document_templates`** (migration `015_document_templates.sql`, model `DocumentTemplate`): `id, name, description, storage_key, original_filename, content_sha256, fields JSONB ([{key,label,type}]), is_active, created_by, created_at, updated_at`. - MinIO bucket **`initiative-templates`** (`S3Settings.s3_bucket_templates`, has a default; created by `ensure_buckets_exist`). Server-side only — no browser CORS / presign needed. ## Backend — `be0/src/template_routes.py` (mounted `/api/v1/templates`) - `POST /templates` (admin) — multipart `.docx` → extract `{{placeholders}}` → MinIO + row. - `GET /templates` (authed; admin sees inactive too) · `GET /{id}` · `GET /{id}/file` (admin, raw docx). - `PUT /{id}` (admin) metadata · `DELETE /{id}` (admin) soft-delete, `?hard=true` removes row + object. - `POST /{id}/render` (authed) — fill with `values` → DOCX (docxtpl) or PDF (reuses `src.be01.docx_to_pdf.convert_docx_bytes_to_pdf`, LibreOffice). - **Placeholder extraction:** docxtpl `get_undeclared_template_variables()` with a regex-on-stripped-XML fallback — DOCX often splits `{{ }}` across `` runs, so stripping tags first recovers them. - Auth mirrors the extracted admin routers (`decode_access_token_user_id`/`decode_bearer_token`; admin = `"admin"` in JWT roles). ## Frontend - **`@ump/shared/lib/templateApi.ts`** — `DocumentTemplate`/`TemplateField` types + list/get/create (FormData)/update/delete/render (`postArrayBuffer`)/downloadFile + `saveArrayBufferAs` / `arrayBufferToObjectUrl`. - **frontend_admin** `pages/TemplatesPage.tsx` (+ `layouts/AdminLayout.tsx` shell; route `/templates`, admin-gated) — upload / list / edit meta / activate-deactivate / download / hard-delete (alert-dialog). Its first real feature. - **frontend_user** `pages/TemplatesFillPage.tsx` (route `/dashboard/templates`, sidebar « Mẫu tài liệu ») — pick a template → form generated from its fields → server PDF → iframe preview + download. ## Verified Backend **e2e live** against the dev stack: create → extract fields → render (filled values) → delete. `@ump/shared` + frontend_user + frontend_admin `tsc --noEmit` + `vite build` all clean. Commits `c6d003c` (BE) + `4f1cb3e` (FE). ## Deferred (v1 limitations) - **No file-replace endpoint** — delete + re-create to change a template's `.docx` (metadata edit works). - **All fields render as single-line text** — no type/date/number/long-text per field. - **No audit-log entries** on template create/update/delete (the rest of the app audits admin mutations).