2026-09-08 14:01:13 +07:00
|
|
|
/* global Word, Office, console */
|
2026-08-19 23:06:57 +07:00
|
|
|
|
|
|
|
|
import * as authService from "./authService";
|
2026-09-08 14:01:13 +07:00
|
|
|
import { getCurrentDocumentFile, getCurrentDocumentPdf } from "./fileService";
|
2026-08-19 23:06:57 +07:00
|
|
|
import {
|
|
|
|
|
generateGuid,
|
|
|
|
|
readContentControlFields,
|
|
|
|
|
readVersionHistoryFromXml,
|
|
|
|
|
stampTrackedProperties,
|
|
|
|
|
writeVersionHistoryToXml,
|
|
|
|
|
XmlVersionEntry,
|
|
|
|
|
} from "./metadataService";
|
|
|
|
|
import { uploadVersion } from "./versioningService";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Orchestration ของ flow หลักสองอันของ add-in (แยกจาก UI เพื่อ unit test ได้):
|
|
|
|
|
*
|
|
|
|
|
* - `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` และ `<Version id>` ใน XML part เก็บ
|
|
|
|
|
* **clientVersionId** (GUID ที่ client generate) เพราะต้อง stamp ลงไฟล์ *ก่อน* upload —
|
|
|
|
|
* versionId ของ backend จะได้มาหลัง upload ซึ่งไฟล์ถูก capture ไปแล้ว
|
|
|
|
|
* (backend คือ source of truth ของ id จริงอยู่ดี)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export interface StartVersioningResult {
|
2026-09-08 14:01:13 +07:00
|
|
|
hasPdf: boolean;
|
2026-08-19 23:06:57 +07:00
|
|
|
documentId: string;
|
|
|
|
|
versionId: string;
|
|
|
|
|
versionNumber: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SaveAndVersionResult {
|
2026-09-08 14:01:13 +07:00
|
|
|
hasPdf: boolean;
|
2026-08-19 23:06:57 +07:00
|
|
|
versionId: string;
|
|
|
|
|
versionNumber: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** เลข version ถัดไปตามที่ client เดา (history ล่าสุด + 1) — ใช้เขียน XML part ก่อนรู้ค่าจริงจาก backend */
|
|
|
|
|
function predictNextVersionNumber(latestVersionNumber: number): number {
|
|
|
|
|
return latestVersionNumber + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** ชื่อไฟล์จาก 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(Word.SaveBehavior.save)` — trigger native save ตาม FR-2 ขั้นตอนที่ 4 */
|
|
|
|
|
async function saveDocument(): Promise<void> {
|
|
|
|
|
await Word.run(async (context) => {
|
|
|
|
|
context.document.save(Word.SaveBehavior.save);
|
|
|
|
|
await context.sync();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function uploadCurrentFile(
|
|
|
|
|
documentId: string,
|
|
|
|
|
clientVersionId: string,
|
2026-08-20 22:05:49 +07:00
|
|
|
fields: Record<string, string>,
|
2026-09-08 14:01:13 +07:00
|
|
|
message?: string,
|
|
|
|
|
onProgress?: (progress: string) => void
|
|
|
|
|
): Promise<SaveAndVersionResult> {
|
2026-08-19 23:06:57 +07:00
|
|
|
const file = await getCurrentDocumentFile();
|
2026-09-08 14:01:13 +07:00
|
|
|
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
|
|
|
|
|
);
|
2026-08-19 23:06:57 +07:00
|
|
|
}
|
|
|
|
|
|
2026-08-20 22:05:49 +07:00
|
|
|
/** FR-1 — เริ่ม track เอกสารปัจจุบัน: stamp รอบแรก + upload เป็น version 1
|
|
|
|
|
* (FR-13: message คือ commit message ทางเลือกจาก dialog ยืนยัน) */
|
2026-08-19 23:06:57 +07:00
|
|
|
export async function startVersioning(
|
2026-08-20 22:05:49 +07:00
|
|
|
message?: string,
|
|
|
|
|
onProgress?: (progress: string) => void
|
2026-08-19 23:06:57 +07:00
|
|
|
): Promise<StartVersioningResult> {
|
|
|
|
|
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 saveDocument();
|
|
|
|
|
onProgress?.("กำลังอ่านค่า fields จากเอกสาร…");
|
|
|
|
|
const fields = await readContentControlFields();
|
|
|
|
|
|
|
|
|
|
// 4) upload version แรก
|
|
|
|
|
onProgress?.("กำลังอัปโหลด version 1…");
|
2026-09-08 14:01:13 +07:00
|
|
|
const result = await uploadCurrentFile(documentId, clientVersionId, fields, message, onProgress);
|
2026-08-19 23:06:57 +07:00
|
|
|
return { documentId, ...result };
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 22:05:49 +07:00
|
|
|
/** FR-2 — Save & Version ของเอกสารที่ track อยู่แล้ว (เรียกหลังผู้ใช้กดยืนยันใน dialog แล้ว)
|
|
|
|
|
* (FR-13: message คือ commit message ทางเลือกจาก dialog ยืนยัน) */
|
2026-08-19 23:06:57 +07:00
|
|
|
export async function saveAndVersion(
|
|
|
|
|
documentId: string,
|
|
|
|
|
latestVersionNumber: number,
|
2026-08-20 22:05:49 +07:00
|
|
|
message?: string,
|
|
|
|
|
onProgress?: (progress: string) => void
|
2026-08-19 23:06:57 +07:00
|
|
|
): Promise<SaveAndVersionResult> {
|
|
|
|
|
const clientVersionId = generateGuid();
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
const predictedNumber = predictNextVersionNumber(latestVersionNumber);
|
|
|
|
|
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 saveDocument();
|
|
|
|
|
onProgress?.("กำลังอ่านค่า fields จากเอกสาร…");
|
|
|
|
|
const fields = await readContentControlFields();
|
|
|
|
|
|
|
|
|
|
// 4) upload
|
|
|
|
|
onProgress?.("กำลังอัปโหลด version ใหม่…");
|
2026-09-08 14:01:13 +07:00
|
|
|
const result = await uploadCurrentFile(documentId, clientVersionId, fields, message, onProgress);
|
2026-08-19 23:06:57 +07:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|