2026-10-16 12:07:25 +07:00
|
|
|
/**
|
|
|
|
|
* MOCK — version API ฝั่ง client (รอ backend จริงตาม contract ใน docs/addin-ui-flow.md §7)
|
|
|
|
|
* ต้นฉบับ: D:\word-addin-poc\addin\src\services\versioningService.ts (multipart POST ผ่าน apiClient)
|
|
|
|
|
*
|
|
|
|
|
* - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ไว้เป๊ะ —
|
|
|
|
|
* วันปลด mock: ก้อบ body ของต้นฉบับกลับมาได้เลย
|
|
|
|
|
* - ต้นฉบับมีอีก 2 ตัวที่ไม่มี caller ใน UI จึงไม่พอร์ต: buildDownloadUrl, downloadVersion
|
2026-10-26 06:29:19 +07:00
|
|
|
* - **วันปลด mock ห้ามส่ง userId/userName/templateId ใน FormData** — contract ของ
|
2026-10-26 10:05:01 +07:00
|
|
|
* POST /api/documents/versions ถอด 3 field นี้ออกแล้ว (ส่งมา = 400) และไม่มี
|
|
|
|
|
* segment {docId} ใน URL อีกต่อไป: server แกะ identity/template และ docId
|
|
|
|
|
* (= fid — fileId ของ e-Court, string) จาก Bearer token (ออกที่ POST
|
|
|
|
|
* /api/documents/token ด้วย x-api-key, fid บังคับ) ที่ apiClient.ts แนบให้อยู่แล้ว
|
2026-10-16 12:07:25 +07:00
|
|
|
* - **metadata ของ version ถูก persist ลง localStorage** (record เล็ก ๆ ต่อ documentId) —
|
|
|
|
|
* ไม่งั้นปิดเอกสาร (taskpane ปิดตาม) แล้วเปิดใหม่ history ว่างเปล่า ทั้งที่ metadata
|
|
|
|
|
* การ track อยู่ในไฟล์จริง ทำให้ทดสอบวง FR-18 ปิด→เปิดใหม่ไม่ได้; blob ของไฟล์จริง
|
|
|
|
|
* ยังเก็บเฉพาะ session (mock download คืน about:blank อยู่แล้ว) — localStorage เข้า
|
|
|
|
|
* ไม่ได้ (webview บางตัว block) = ใช้ memory อย่างเดียวแบบเดิม ไม่ throw
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as authService from "./authService";
|
|
|
|
|
import { generateGuid } from "./metadataService";
|
|
|
|
|
import { UploadVersionResult, VersionRecord } from "../models/versionRecord";
|
|
|
|
|
|
|
|
|
|
interface StoredVersion {
|
|
|
|
|
record: VersionRecord;
|
|
|
|
|
docxBlob: Blob | null;
|
|
|
|
|
pdfBlob: Blob | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** key = documentId, เก็บเรียงเก่า → ใหม่ */
|
|
|
|
|
const store = new Map<string, StoredVersion[]>();
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY = "wordaddin-mock-versions";
|
|
|
|
|
|
|
|
|
|
/** โหลด record ที่ persist ไว้ (เฉพาะ metadata — blob ไม่ข้าม session) — พังใด ๆ = เริ่มว่าง */
|
|
|
|
|
function loadPersisted(): Record<string, VersionRecord[]> {
|
|
|
|
|
try {
|
|
|
|
|
if (typeof window === "undefined") return {}; // SSR ของ client component — ไม่มี localStorage
|
|
|
|
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
|
|
|
const parsed = raw ? (JSON.parse(raw) as Record<string, VersionRecord[]>) : {};
|
|
|
|
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
|
|
|
} catch {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const persisted = loadPersisted();
|
|
|
|
|
for (const [documentId, records] of Object.entries(persisted)) {
|
|
|
|
|
if (Array.isArray(records)) {
|
|
|
|
|
store.set(documentId, records.map((record) => ({ record, docxBlob: null, pdfBlob: null })));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** บันทึกเฉพาะ metadata กลับลง localStorage — พัง = เงียบ (ยังมี memory ของ session นี้อยู่) */
|
|
|
|
|
function persist(): void {
|
|
|
|
|
try {
|
|
|
|
|
const snapshot: Record<string, VersionRecord[]> = {};
|
|
|
|
|
store.forEach((versions, documentId) => {
|
|
|
|
|
snapshot[documentId] = versions.map((item) => item.record);
|
|
|
|
|
});
|
|
|
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore — quota/private mode
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** SHA-256 จริงของไฟล์ที่ได้จาก Word — พังก็ไม่ throw (ใช้ค่าหยง ๆ ให้ UI ยังไปต่อได้) */
|
|
|
|
|
async function mockContentHash(file: Blob): Promise<string> {
|
|
|
|
|
try {
|
|
|
|
|
const digest = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
|
|
|
|
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
|
|
|
} catch {
|
|
|
|
|
return "mock-hash";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** FR-5 — อัปโหลดไฟล์ปัจจุบันเป็น version ใหม่ (mock: เก็บลง memory)
|
|
|
|
|
* + FR-13: message คือ commit message ทางเลือก
|
|
|
|
|
* + FR-16: templateId เฉพาะไฟล์ร่าง (mock ไม่ใช้ — คงไว้ตาม signature ต้นฉบับ) */
|
|
|
|
|
export async function uploadVersion(
|
|
|
|
|
documentId: string,
|
|
|
|
|
file: Blob,
|
|
|
|
|
clientVersionId: string,
|
|
|
|
|
fields?: Record<string, string>,
|
|
|
|
|
fileName?: string,
|
|
|
|
|
message?: string,
|
|
|
|
|
pdf?: Blob,
|
|
|
|
|
templateId?: string
|
|
|
|
|
): Promise<UploadVersionResult> {
|
|
|
|
|
void clientVersionId; // mock ไม่ใช้ — id ของ mock สร้างเอง
|
|
|
|
|
void fileName;
|
|
|
|
|
void templateId;
|
|
|
|
|
await delay(500); // ให้ข้อความ progress ของ flow ขึ้นจริง
|
|
|
|
|
|
|
|
|
|
const versions = store.get(documentId) ?? [];
|
|
|
|
|
const versionNumber = versions.reduce((max, item) => Math.max(max, item.record.versionNumber), 0) + 1;
|
|
|
|
|
const hasPdf = !!pdf && pdf.size > 0;
|
|
|
|
|
const trimmedMessage = message?.trim();
|
|
|
|
|
|
|
|
|
|
const record: VersionRecord = {
|
|
|
|
|
versionId: generateGuid(),
|
|
|
|
|
versionNumber,
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
createdByUserId: authService.getUsername() ?? "unknown",
|
|
|
|
|
contentHash: await mockContentHash(file),
|
|
|
|
|
message: trimmedMessage || null,
|
|
|
|
|
hasPdf,
|
|
|
|
|
fields: fields && Object.keys(fields).length > 0 ? fields : null,
|
|
|
|
|
};
|
|
|
|
|
versions.push({ record, docxBlob: file, pdfBlob: pdf ?? null });
|
|
|
|
|
store.set(documentId, versions);
|
|
|
|
|
persist();
|
|
|
|
|
return { hasPdf, versionId: record.versionId, versionNumber };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** FR-6 — version history เรียงใหม่ → เก่า (คืนสำเนากัน caller mutate store ตรง ๆ) */
|
|
|
|
|
export async function getVersions(documentId: string): Promise<VersionRecord[]> {
|
|
|
|
|
await delay(200);
|
|
|
|
|
const versions = store.get(documentId) ?? [];
|
|
|
|
|
return versions
|
|
|
|
|
.slice()
|
|
|
|
|
.reverse()
|
|
|
|
|
.map((item) => ({ ...item.record }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface VersionDownloadLink {
|
|
|
|
|
url: string;
|
|
|
|
|
/** ISO-8601 เวลาที่ URL หมดอายุ */
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FR-7 — ขอ presigned URL อายุสั้นของ version หนึ่ง เพื่อเปิดในเบราว์เซอร์ของผู้ใช้
|
|
|
|
|
* (webview ของ Word ทิ้ง blob download เงียบๆ — ให้เบราว์เซอร์จริงจัดการแทน)
|
|
|
|
|
* mock: ไม่มี URL จริงให้คืน — blob:/data: ใช้ผ่าน openBrowserWindow ไม่ได้ (blob ผูกกับ
|
|
|
|
|
* context ของ webview, data: โดนเบราว์เซอร์ block) ส่วน about:blank ที่คืนไปนี้ host จะ
|
|
|
|
|
* ปฏิเสธเงียบๆ เพราะ openBrowserWindow เปิดเฉพาะ http/https และเป็น void API ไม่รายงานกลับ
|
|
|
|
|
* — กดปุ่มดาวน์โหลด/PDF ตอน mock จึงไม่มีหน้าต่างเบราว์เซอร์เปิด (UI ยังเดินครบ flow)
|
|
|
|
|
*/
|
|
|
|
|
export async function getDownloadLink(
|
|
|
|
|
documentId: string,
|
|
|
|
|
versionId: string,
|
|
|
|
|
format?: "docx" | "pdf"
|
|
|
|
|
): Promise<VersionDownloadLink> {
|
|
|
|
|
void documentId;
|
|
|
|
|
void versionId;
|
|
|
|
|
void format;
|
|
|
|
|
await delay(150);
|
|
|
|
|
return { url: "about:blank", expiresAt: new Date(Date.now() + 10 * 60_000).toISOString() };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** สร้าง clientVersionId ใหม่สำหรับ upload ครั้งถัดไป */
|
|
|
|
|
export function newClientVersionId(): string {
|
|
|
|
|
return generateGuid();
|
|
|
|
|
}
|