/* global Office, console */ /** * Orchestration ของ flow หลักสองอันของ add-in (แยกจาก UI เพื่อ unit test ได้): * พอร์ตดิบจาก D:\word-addin-poc\addin\src\services\versionFlow.ts * * - `startVersioning` (FR-1 "Start Versioning This Document") — เอกสารใหม่ที่ยังไม่ถูก track * - `saveAndVersion` (FR-2) — ไล่ตามลำดับใน spec แบบเคร่งครัด: * stamp properties → XML part (delete-then-add, fail ได้เงียบๆ) → document.save() * → ดึงไฟล์ + extract fields (FR-11) → POST version → คืนผลให้ UI แสดง/refresh history * * หมายเหตุค่า id ใน metadata: `LastVersionId` และ `` ใน XML part เก็บ * **clientVersionId** (GUID ที่ client generate) เพราะต้อง stamp ลงไฟล์ *ก่อน* upload — * versionId ของ backend จะได้มาหลัง upload ซึ่งไฟล์ถูก capture ไปแล้ว * (backend คือ source of truth ของ id จริงอยู่ดี) */ import * as authService from "./authService"; import { autoSaveService } from "./autoSaveService"; import { DocumentCloseMode, closeDocument, saveDocumentVerified } from "./documentService"; import { getCurrentDocumentFile, getCurrentDocumentPdf } from "./fileService"; import { generateGuid, readContentControlFields, readVersionHistoryFromXml, stampTrackedProperties, writeVersionHistoryToXml, XmlVersionEntry, } from "./metadataService"; import { uploadVersion } from "./versioningService"; export interface StartVersioningResult { hasPdf: boolean; documentId: string; versionId: string; versionNumber: number; } export interface SaveAndVersionResult { hasPdf: boolean; versionId: string; versionNumber: number; } /** * FR-18 — version ถูกบันทึกสำเร็จแล้วแต่ปิดเอกสารไม่สำเร็จ (เอกสารยังเปิดอยู่) * UI ต้องแยกสถานะนี้ออกจาก "สร้าง version ไม่สำเร็จ" — version ปลอดภัยบน backend แล้ว */ export class VersionSavedButCloseFailedError extends Error { constructor( public readonly result: SaveAndVersionResult, cause: unknown ) { super( `บันทึก version ${result.versionNumber} เรียบร้อย แต่ปิดเอกสารไม่สำเร็จ: ${ cause instanceof Error ? cause.message : String(cause) } — ปิดเอกสารเองได้เลย` ); this.name = "VersionSavedButCloseFailedError"; } } /** ชื่อไฟล์จาก URL ของ document ถ้าดึงได้ (desktop มักเป็น path จริง) — ใช้เป็นชื่อตอน upload */ function guessDocumentFileName(): string | undefined { try { const url = Office.context.document.url; if (!url) { return undefined; } const name = url.split(/[\\/]/).pop(); if (!name) { return undefined; } return name.toLowerCase().endsWith(".docx") ? name : `${name}.docx`; } catch { return undefined; } } /** `context.document.save()` ย้ายไป documentService (ใช้ร่วมกับ FR-17 auto-save) */ async function uploadCurrentFile( documentId: string, clientVersionId: string, fields: Record, message?: string, onProgress?: (progress: string) => void, templateId?: string ): Promise { const file = await getCurrentDocumentFile(); let pdf: Blob | undefined; onProgress?.("กำลังสร้างสำเนา PDF…"); try { pdf = await getCurrentDocumentPdf(); if (pdf.size === 0) throw new Error("Word คืนไฟล์ PDF ว่าง"); } catch (error) { console.warn("PDF capture failed:", error); pdf = undefined; } onProgress?.("กำลังอัปโหลด version…"); return uploadVersion( documentId, file, clientVersionId, fields, guessDocumentFileName(), message, pdf, templateId ); } /** FR-1 — เริ่ม track เอกสารปัจจุบัน: stamp รอบแรก + upload เป็น version 1 * (FR-13: message คือ commit message ทางเลือกจาก dialog ยืนยัน) * (FR-16: templateId ส่งเฉพาะไฟล์ร่างที่เว็บสร้าง — backend ผูก Document กับ template ต้นทางตอน auto-create) */ export async function startVersioning( message?: string, templateId?: string, onProgress?: (progress: string) => void ): Promise { // FR-17 — หยุด auto-save ตลอด flow กัน save แทรกกลาง stamp/capture แล้วค่อนคืนตอนจบ await autoSaveService.suspend(); try { const documentId = generateGuid(); const clientVersionId = generateGuid(); const timestamp = new Date().toISOString(); // 1) stamp รอบแรก — ได้ทั้ง SavedByAddIn และ DocumentTrackingId ใหม่ onProgress?.("กำลังบันทึก metadata ของเอกสาร…"); await stampTrackedProperties({ documentTrackingId: documentId, lastVersionId: clientVersionId, lastVersionTimestamp: timestamp, }); // 2) XML part (secondary — fail ได้เงียบๆ) onProgress?.("กำลังอัปเดต version history ในไฟล์…"); await writeVersionHistoryToXml([ { id: clientVersionId, number: 1, timestamp, user: authService.getUsername() ?? "unknown" }, ]); // 3) native save แล้ว capture ไฟล์ + fields onProgress?.("กำลังบันทึกไฟล์…"); await saveDocumentVerified(); onProgress?.("กำลังอ่านค่า fields จากเอกสาร…"); const fields = await readContentControlFields(); // 4) upload version แรก (แนบ templateId เมื่อเป็นไฟล์ร่าง — FR-16) onProgress?.("กำลังอัปโหลด version 1…"); const result = await uploadCurrentFile(documentId, clientVersionId, fields, message, onProgress, templateId); // FR-3 — สร้าง version สำเร็จ → เคลียร์ flag "มีแก้ไขที่ยังไม่เป็น version" autoSaveService.clearUncommittedSinceVersion(); return { documentId, ...result }; } finally { autoSaveService.resume(); } } /** FR-2 — Save & Version ของเอกสารที่ track อยู่แล้ว (เรียกหลังผู้ใช้กดยืนยันใน dialog แล้ว) * (FR-13: message คือ commit message ทางเลือกจาก dialog ยืนยัน) * (FR-18: closeAfter=true — หลัง upload สำเร็จเท่านั้น ปิดเอกสารด้วย close(Save); * ปิดไม่สำเร็จโยน VersionSavedButCloseFailedError ไม่ใช่ error ของ version) */ export async function saveAndVersion( documentId: string, latestVersionNumber: number, message?: string, onProgress?: (progress: string) => void, closeAfter = false ): Promise { // FR-17 — หยุด auto-save ตลอด flow กัน save แทรกกลาง stamp/capture แล้วค่อนคืนตอนจบ await autoSaveService.suspend(); try { const clientVersionId = generateGuid(); const timestamp = new Date().toISOString(); const predictedNumber = latestVersionNumber + 1; const user = authService.getUsername() ?? "unknown"; // 1) stamp properties (SavedByAddIn + DocumentTrackingId ถูก stamp ไว้แล้วจากรอบแรก — เขียนทับไม่เสียหาย) onProgress?.("กำลังบันทึก metadata ของ version ใหม่…"); await stampTrackedProperties({ lastVersionId: clientVersionId, lastVersionTimestamp: timestamp, }); // 2) XML part: เพิ่ม entry ของ version ใหม่ไว้ต้นสุด=ใหม่→เก่า ตามรูปใน spec (เดาเลขก่อน upload) — fail ได้เงียบๆ onProgress?.("กำลังอัปเดต version history ในไฟล์…"); const entries = await readVersionHistoryFromXml(); entries.unshift({ id: clientVersionId, number: predictedNumber, timestamp, user }); await writeVersionHistoryToXml(entries); // 3) native save → capture ไฟล์ + fields ปัจจุบัน (FR-11) onProgress?.("กำลังบันทึกไฟล์…"); await saveDocumentVerified(); onProgress?.("กำลังอ่านค่า fields จากเอกสาร…"); const fields = await readContentControlFields(); // 4) upload onProgress?.("กำลังอัปโหลด version ใหม่…"); const result = await uploadCurrentFile(documentId, clientVersionId, fields, message, onProgress); // 5) เลขจริงจาก backend ต่างจากที่เดา (มี client อื่นแทรก) → แก้ XML part ให้ตรง (best effort) if (result.versionNumber !== predictedNumber) { const corrected = await readVersionHistoryFromXml(); const entry = corrected.find((item: XmlVersionEntry) => item.id === clientVersionId); if (entry) { entry.number = result.versionNumber; corrected.sort((a, b) => b.number - a.number); // คงลำดับใหม่→เก่าหลังแก้เลข await writeVersionHistoryToXml(corrected); } } // FR-3 — สร้าง version สำเร็จ → เคลียร์ flag "มีแก้ไขที่ยังไม่เป็น version" autoSaveService.clearUncommittedSinceVersion(); // 6) FR-18 — ปิดเอกสารเฉพาะเมื่อ upload สำเร็จแล้วเท่านั้น (ต้องอยู่หลังขั้น 5 เพราะหลัง close // เอกสารหมดสิทธิ์เรียก Word.run ต่อ) if (closeAfter) { try { await closeDocument(DocumentCloseMode.Save); } catch (error) { throw new VersionSavedButCloseFailedError(result, error); } } return result; } finally { autoSaveService.resume(); } }