eCourt-Doc-Edit/docs/plans/fr16-draft-generation-plan.md

94 lines
15 KiB
Markdown
Raw Permalink Normal View History

# แผนงาน: ไฟล์ร่าง (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` มีอยู่แล้ว)