eCourt-Doc-Edit/docs/plans/fr16-draft-generation-plan.md
Suphonchai Phoonsawat b3c1c667f7 Phase 12: draft generation (FR-16)
ไฟล์ร่าง (draft) — generate docx จาก template แบบ stateless ไม่สร้าง
Document/version; stamp เฉพาะ custom property TemplateId เพื่อให้ add-in
เห็นเป็น untracked; Start Versioning ส่ง form field templateId ให้ backend
ผูก template ตอน auto-create — เอกสารเริ่มที่ v1 (ไม่มี v0) + field sync เต็มระบบ

- Backend: POST /api/templates/{id}/generate-draft (200 คืนไฟล์) +
  DocxGenerationService.GenerateDraft + แยก TemplateFieldValidation ใช้ร่วมกับ
  generate + UploadVersion รับ templateId (non-GUID/unknown → 400,
  deactivated → ผูกได้, document เดิม → เฉย) + CORS expose Content-Disposition
- Add-in: readTrackedProperties อ่าน TemplateId + ส่ง templateId เฉพาะ
  startVersioning + hint ไฟล์ร่างใน task pane
- Web: ปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม + downloadFile รับ init (POST → blob)
- Docs: spec v8 (FR-16, AC-19) + Phase 12 + CONTEXT.md + ADR 0002 +
  acceptance-review AC-19
- Tests: backend 82 / add-in 66 / web 59 + build ครบ; QA reviewer PASS
  (2 rounds); ค้าง manual บน Word จริง

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 10:51:13 +07:00

93 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# แผนงาน: ไฟล์ร่าง (draft) — generate docx จาก template โดยยังไม่สร้าง version (FR-16 / spec v8 / Phase 12)
> สถานะ: **implement แล้ว (2026-09-10)** — ติดตามผลที่ `officejs-implementation-plan.md` Phase 12 และ `acceptance-review.md` AC-19 (ค้าง manual บน Word จริง)
> แผนนี้เก็บไว้เป็นหลักฐานการตัดสินใจของ grill session — รายละเอียด decision เพิ่มที่ `docs/adr/0002-draft-stateless.md`
## Context
ปัจจุบันการสร้างเอกสารจาก template ผ่านเว็บ (`POST /api/documents/generate`, FR-10) สร้าง `Document` + field values + **v0 ฉบับตั้งต้น** ทันที แม้ผู้ใช้จะยังไม่เคยเปิดไฟล์ใน Word
Flow ใหม่: ฟอร์มเว็บกรอกค่า → กด "สร้างไฟล์ร่าง" → API คืน docx → เปิดใน Word → add-in เห็น untracked → กด Start Versioning → เกิด `Document` (ผูก template) + **v1** + field sync
**การทบทวน version numbering (ไม่ต้องแก้โค้ด):** `UploadVersion` คำนวณ `(max ?? 0) + 1` ที่ `backend/WordVersioning.Api/Controllers/DocumentsController.cs:140-144` — Document ใหม่ไม่มี version → `max = null` → **v1** อัตโนมัติ มี test เดิมยืนยันแล้ว · เพิ่ม test ยืนยันผ่าน TemplateLinkingTests (ด้านล่าง)
## Decisions (จาก grill session — final)
| # | หัวข้อ | การตัดสินใจ |
|---|---|---|
| 1 | การเก็บ draft | **Stateless** — POST คืน docx stream ตรงๆ ไม่สร้าง Document/version ไม่แตะ MinIO · เว็บ browser-download · ไม่มีปุ่ม "เปิดใน Word" (ms-word:ofe ต้องการ URL ซึ่ง draft ไม่มี) |
| 2 | Stamping | Stamp custom property **`TemplateId` เท่านั้น** — ไม่มี SavedByAddIn/DocumentTrackingId (กันสถานะ "tracking + history 404" ซึ่ง add-in จัดการไม่ได้ในปัจจุบัน) |
| 3 | กลไกผูก template ตอน Start Versioning | ขยาย `POST /api/documents/{documentId}/versions` เพิ่ม form field ทางเลือก `templateId` — ใช้เฉพาะตอน auto-create Document; document มีอยู่แล้ว → เฉย (validate ทุกครั้งที่ส่งมา) |
| 4 | Validation | upload: ไม่ใช่ GUID → 400 · ไม่พบ template → 400 · **deactivated → ผูกได้** · draft endpoint เอง: unknown → 404, deactivated → 400 ตามนโยบาย FR-10 (deactivated สร้างไฟล์ใหม่ไม่ได้ แต่ draft เก่าที่สร้างตอน active ผูกต่อได้) |
| 5 | Web UI | **คง flow v0** + เพิ่มปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม · re-generate (FR-12) ใช้ v0 flow เดิม |
| 6 | ศัพท์ | **"ไฟล์ร่าง (draft)"** ทั้งระบบ (UI/spec/CONTEXT.md) |
## Endpoint design
**`POST /api/templates/{templateId:guid}/generate-draft`** (ใน TemplatesController — draft เป็น stateless function ของ template; controller นี้มี DI ครบและมี precedent sub-resource `{id}/documents` แล้ว)
- Request: `GenerateDraftRequest { [Required] JsonElement Values }` (ใน `Models/TemplateDtos.cs`) — ไม่มี `message` (ไม่มี version จึงไม่มี commit message)
- Response: **200** `FileContentResult` — `File(bytes, docxContentType, BuildGeneratedFileName(template.Name, now))` (ไม่ใช่ 201 — ไม่มีอะไรถูกสร้าง)
- Status: unknown template → 404 · `!IsActive` → 400 · values ผิด format/required/type → 400 (ข้อความเดิมของ Generate) · template file หายใน MinIO/corrupt → 502
## Implementation
### 1. Backend
1. `Services/AddInMetadata.cs` — เพิ่ม `TemplateIdPropertyName = "TemplateId"`
2. `Services/DocxGenerationService.cs` — แยก SDT-replace loop ของ `Generate` เป็น `private static ReplaceSdtValues(...)` ใช้ร่วม · เพิ่ม `byte[] GenerateDraft(Stream templateStream, IReadOnlyDictionary<string,string> values, Guid templateId)` (replace เหมือนเดิม + stamp **เฉพาะ** `TemplateId`) · ย้าย `BuildGeneratedFileName` จาก DocumentsController มาเป็น `public static` ที่นี่
3. `Services/TemplateFieldValidation.cs` (ไฟล์ใหม่) — ยก required/type validation block จาก `Generate` (~lines 260-285) มาเป็น `static string? Validate(IEnumerable<TemplateField>, IReadOnlyDictionary<string,string?>)` (ข้อความ error ไทยต้องเหมือนเดิมเป๊ะ — `GenerateTests` assert ไว้)
4. `Controllers/DocumentsController.cs`:
- `Generate` เปลี่ยนไปใช้ validator + filename ที่แยกออกมาแล้ว
- `UploadVersion`: early validation `request.TemplateId` (ไม่ใช่ GUID → 400) → หลัง doc lookup ถ้ามี templateId ให้ `db.Templates.AnyAsync` (ไม่พบ → 400; deactivated ผ่าน) → ใส่ `TemplateId = templateIdToLink` ใน auto-create initializer (~lines 105-111) **ก่อน** branch `doc.TemplateId is not null` (~line 120) ไม่งั้น field sync จะหลุดเป็น raw-map path
5. `Models/VersionDtos.cs` — `UploadVersionRequest` เพิ่ม `string? TemplateId`
6. `Controllers/TemplatesController.cs` — action ใหม่ `GenerateDraft(Guid id, [FromBody] GenerateDraftRequest, CancellationToken)`: load template (404/400) → load field defs → `FieldMapJson.TryNormalize` → `TemplateFieldValidation.Validate` → load template stream (502) → `GenerateDraft(...)` → log → `File(...)`
7. `Program.cs` — CORS เพิ่ม `.WithExposedHeaders("Content-Disposition")` (**จำเป็น** — draft download อ่านชื่อไฟล์ไทยจาก header นี้ผ่าน fetch; ปัจจุบันไม่ได้ expose ชื่อจะหายเงียบๆ — integration tests จับไม่ได้เพราะ same-origin)
### 2. Add-in
8. `addin/src/services/metadataService.ts` — `TEMPLATE_ID_PROPERTY` + `templateId?: string` ใน `TrackedDocumentProperties` + อ่านเป็น property ที่ 5 ใน `readTrackedProperties()` (Word.run เดิม)
9. `addin/src/services/versioningService.ts` — `uploadVersion(..., pdf?, templateId?)` (trailing param, caller เดิมไม่กระทบ) append form field เมื่อมีค่า
10. `addin/src/services/versionFlow.ts` — `startVersioning(message?, templateId?, onProgress?)` (แทรกก่อน callback — TS จะจับ call site เก่าทั้งหมดให้แก้) · `saveAndVersion` **ไม่ส่ง** templateId
11. `addin/src/taskpane/components/App.tsx` — state `documentTemplateId` · **restructure `detect()`** เป็น `readTrackedProperties()` ครั้งเดียวแล้ว derive tracked เอง (ถ้าคงเรียก `isTrackedDocument()` ก่อน ไฟล์ร่าง untracked จะไม่ได้อ่าน TemplateId เลย) · เคลียร์ state ตอน sign-out/detect ใหม่ · untracked phase แสดง hint เมื่อเป็นไฟล์ร่าง: "ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกกับ template ต้นทางและ sync ค่า fields ให้อัตโนมัติ" · `handleStartVersioning` ส่ง `documentTemplateId ?? undefined`
### 3. Web
12. `web/app/composables/useApi.ts` — `downloadFile(path, fallbackName, init: RequestInit = {})` ส่ง init ต่อให้ `request` (รองรับ POST+JSON→blob; ปัจจุบัน GET-only และไม่มี caller — non-breaking)
13. `web/app/pages/documents/create/[templateId].vue` — state `draftSubmitting`/`draftNotice` · `submitDraft()`: validate ด้วย `validateFieldValues` เดิม → `downloadFile('/api/templates/{id}/generate-draft', fallback, { method:'POST', body: JSON.stringify({ values }) })` → notice แนะนำเปิดใน Word แล้วกด Start Versioning · **ฟอร์มอยู่ต่อ** (ไม่มี result card, ไม่มีปุ่มเปิดใน Word) · ปุ่มรอง "สร้างไฟล์ร่าง" (`type="button"`, disabled gating เดียวกับปุ่มหลักรวม `!detail.isActive`) · helper text ใต้ช่องคำอธิบาย "(ใช้กับการสร้างเอกสาร version 0 เท่านั้น — ไฟล์ร่างไม่มี version)" · `resetForm()` เคลียร์ `draftNotice`
### 4. Docs (repo rule: spec ก่อน แล้ว sync plan)
14. `specs/spec-officejs.md` — v8 header note · `### FR-16: ไฟล์ร่าง (draft) — สร้าง .docx จาก template โดยไม่สร้าง Document (เพิ่มเติม v8)` · section 8 metadata table เพิ่มแถว `TemplateId` · AC-19 · section เพิ่มเติม v8
15. `docs/plans/officejs-implementation-plan.md` — append `## Phase 12: ไฟล์ร่าง (draft) — FR-16 (เพิ่มทีหลัง, v8)` ท้ายไฟล์ (รูปแบบเดียวกับ Phase 11: ตาราง mapping + checklist + DoD)
16. `CONTEXT.md` — glossary เพิ่ม: `- **ไฟล์ร่าง (draft)**: ไฟล์ .docx ที่สร้างจาก template แบบ stateless — ยังไม่มี Document/version ในระบบ; ระบุ template ต้นทางผ่าน custom property TemplateId และเริ่ม track จริงตอนกด Start Versioning ใน Word (version 1)`
17. `docs/adr/0002-draft-stateless.md` (ไฟล์ใหม่ — format ตาม ADR 0001): Decision (stateless endpoint, stamp TemplateId ตัวเดียว, ผูก template ทีหลังผ่าน form field) + Alternatives rejected (สร้าง Document สถานะร่าง + lifecycle/cleanup / stamp tracked props เต็มชุด = สถานะ "tracking + 404" พัง / ปุ่ม "เปิดใน Word" สำหรับ draft) + Consequences (draft หาย = ไม่มีของค้างในระบบ, v1 ไม่มี v0, field sync เริ่ม v1, ต้อง expose Content-Disposition)
18. `docs/acceptance-review.md` — แถว AC-19 · `CLAUDE.md` — sync สถานะ (FR-1–16, AC 1–19, Phase 0–12) + ย่อหน้าสถาปัตยกรรมสั้นๆ หลัง section FR-15
## Tests
**Backend (integration — Testcontainers, `[Collection(IntegrationTestCollection.Name)]`):**
- `DraftTests.cs` (ใหม่): happy path 200 + ค่า SDT ครบ (ใช้ `TestDocx.ReadSdtText`) + `ReadCustomProperty("TemplateId")` ตรง + `SavedByAddIn`/`DocumentTrackingId` เป็น null + **DB ไม่เกิด Document/version ใดๆ** · missing-required 400 · wrong-type 400 · unknown template 404 · deactivated 400 · extra keys ignored
- `TemplateLinkingTests.cs` (ใหม่): upload ใหม่ + templateId → 201 **VersionNumber=1**, `doc.TemplateId` ชัด, `AddInSignature=""`, `DocumentFieldValue` ครบ (SourceVersionId/UpdatedByUserId ถูก), snapshot non-null · upload ครั้งสอง (ไม่ส่ง) → v2 · ไม่ใช่ GUID → 400 · unknown GUID → 400 · deactivated → ผูกได้ · document เดิม + templateId → เฉย (TemplateId คงเดิม) · document เดิม + templateId เสีย → 400
- `TestDtos.cs` — `VersionForm` เพิ่ม optional `templateId` ต่อท้าย (caller เดิมไม่กระทบ) · `AuthTests.cs` — เพิ่ม `[InlineData("POST", "api/templates/{0}/generate-draft")]`
**Add-in (jest):** metadataService อ่าน templateId ได้ (ปรับ expectation `toEqual` ของ `readTrackedProperties` เดิม) · versionFlow ส่ง templateId เฉพาะ startVersioning ไม่ใช่ saveAndVersion · versioningService append form field
**Web (vitest):** `web/test/useApi.spec.ts` (ใหม่ — pattern ตาม `useVersionOpen.spec.ts`): POST init ส่งถึง fetch + Bearer · 401 retry ครั้งเดียว · ชื่อไฟล์ `filename*` UTF-8 ไทย ไปอยู่ที่ anchor download · !ok → ApiError พร้อมข้อความจาก backend
## Verification (DoD)
```bash
cd backend && dotnet test # เขียวทั้งชุด (รวม suite เดิมที่ไม่ถูกแตะ)
cd addin && npm test
cd web && npm test && npm run build
```
Manual บน Word for Mac: สร้างไฟล์ร่างจากเว็บ → ได้ .docx ชื่อไทยถูกต้อง → เปิดใน Word → task pane แสดง untracked + hint ไฟล์ร่าง → กด Start Versioning → ได้ v1 + หน้า document detail บนเว็บแสดง template และค่า fields · Save & Version ถัดไป → v2
## Risks
- **Content-Disposition ไม่ถูก CORS-expose** — จับไม่ได้ด้วย integration tests (same-origin) ครอบด้วย useApi.spec + manual check
- ลำดับแทรกใน UploadVersion สำคัญ (TemplateId ต้อง set ก่อน field-sync branch) — TemplateLinkingTests คุมไว้
- `startVersioning` เปลี่ยน positional signature — TS จับ call site ทั้งหมด แก้ให้ครบใน commit เดียวกัน
- ไม่มี DB migration ใดๆ (ไม่แตะ schema — `UploadVersionRequest.TemplateId` เป็น form field, คอลัมน์ `Document.TemplateId` มีอยู่แล้ว)