// Shared helpers cho 5 file user manual (02-06) const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, Header, Footer, AlignmentType, LevelFormat, HeadingLevel, BorderStyle, WidthType, ShadingType, PageNumber, TableOfContents, TabStopType, TabStopPosition, } = require('docx'); const PAGE_WIDTH = 12240, PAGE_HEIGHT = 15840, MARGIN = 1440; const CONTENT_WIDTH = PAGE_WIDTH - MARGIN * 2; // 9360 const FONT = "Calibri"; const BRAND = "1F7DC1"; // ===== Heading helpers ===== const h1 = (text) => new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, font: FONT, size: 32, bold: true, color: BRAND })], spacing: { before: 360, after: 200 }, }); const h2 = (text) => new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, font: FONT, size: 26, bold: true, color: "333333" })], spacing: { before: 280, after: 160 }, }); const h3 = (text) => new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text, font: FONT, size: 22, bold: true, color: "555555" })], spacing: { before: 200, after: 120 }, }); // ===== Text helpers ===== const p = (text, opts = {}) => new Paragraph({ children: [new TextRun({ text, font: FONT, size: 22, ...(opts.run || {}) })], spacing: { after: 120 }, ...opts, }); const bullet = (text, level = 0) => new Paragraph({ numbering: { reference: "bullets", level }, children: [new TextRun({ text, font: FONT, size: 22 })], spacing: { after: 80 }, }); const num = (text, level = 0) => new Paragraph({ numbering: { reference: "numbers", level }, children: [new TextRun({ text, font: FONT, size: 22 })], spacing: { after: 80 }, }); // ===== Callout boxes ===== const note = (text) => new Paragraph({ shading: { fill: "FFF4D6", type: ShadingType.CLEAR }, border: { left: { style: BorderStyle.SINGLE, size: 18, color: "F0B429" } }, children: [new TextRun({ text: "💡 " + text, font: FONT, size: 22, italics: true })], spacing: { before: 120, after: 120 }, indent: { left: 240 }, }); const warn = (text) => new Paragraph({ shading: { fill: "FFE5E5", type: ShadingType.CLEAR }, border: { left: { style: BorderStyle.SINGLE, size: 18, color: "D63031" } }, children: [new TextRun({ text: "⚠️ " + text, font: FONT, size: 22, italics: true })], spacing: { before: 120, after: 120 }, indent: { left: 240 }, }); const tip = (text) => new Paragraph({ shading: { fill: "E8F5E9", type: ShadingType.CLEAR }, border: { left: { style: BorderStyle.SINGLE, size: 18, color: "4CAF50" } }, children: [new TextRun({ text: "✓ " + text, font: FONT, size: 22, italics: true })], spacing: { before: 120, after: 120 }, indent: { left: 240 }, }); const code = (text) => new Paragraph({ shading: { fill: "F5F5F5", type: ShadingType.CLEAR }, children: [new TextRun({ text, font: "Consolas", size: 20 })], spacing: { before: 80, after: 120 }, indent: { left: 240 }, }); // ===== Mock screenshot placeholder ===== const mockScreenshot = (label) => new Paragraph({ alignment: AlignmentType.CENTER, shading: { fill: "FAFAFA", type: ShadingType.CLEAR }, border: { top: { style: BorderStyle.DASHED, size: 6, color: "999999" }, bottom: { style: BorderStyle.DASHED, size: 6, color: "999999" }, left: { style: BorderStyle.DASHED, size: 6, color: "999999" }, right: { style: BorderStyle.DASHED, size: 6, color: "999999" }, }, spacing: { before: 200, after: 200 }, children: [new TextRun({ text: `[ Ảnh chụp màn hình: ${label} ]`, font: FONT, size: 20, italics: true, color: "999999", })], }); // ===== Table cell builder ===== const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; const borders = { top: border, bottom: border, left: border, right: border }; const cellMargins = { top: 80, bottom: 80, left: 120, right: 120 }; const cell = (text, opts = {}) => new TableCell({ borders, width: { size: opts.width, type: WidthType.DXA }, shading: opts.shading ? { fill: opts.shading, type: ShadingType.CLEAR } : undefined, margins: cellMargins, children: [new Paragraph({ children: [new TextRun({ text, font: FONT, size: 20, bold: opts.bold || false, color: opts.color || "000000", })], })], }); // ===== Bảng nhập liệu chuẩn 5 cột ===== const fieldTable = (rows) => { const widths = [2200, 1500, 1100, 2860, 1700]; return new Table({ width: { size: CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: widths, rows: [ new TableRow({ tableHeader: true, children: [ cell("Tên field", { width: widths[0], bold: true, shading: BRAND, color: "FFFFFF" }), cell("Kiểu dữ liệu", { width: widths[1], bold: true, shading: BRAND, color: "FFFFFF" }), cell("Bắt buộc", { width: widths[2], bold: true, shading: BRAND, color: "FFFFFF" }), cell("Quy tắc validation", { width: widths[3], bold: true, shading: BRAND, color: "FFFFFF" }), cell("Ví dụ", { width: widths[4], bold: true, shading: BRAND, color: "FFFFFF" }), ]}), ...rows.map(r => new TableRow({ children: [ cell(r[0], { width: widths[0], bold: true }), cell(r[1], { width: widths[1] }), cell(r[2], { width: widths[2], color: r[2] === "Có" ? "D63031" : "999999", bold: r[2] === "Có" }), cell(r[3], { width: widths[3] }), cell(r[4], { width: widths[4] }), ]})), ], }); }; // ===== Bảng lỗi 3 cột ===== const errorTable = (rows) => { const widths = [2800, 3360, 3200]; return new Table({ width: { size: CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: widths, rows: [ new TableRow({ tableHeader: true, children: [ cell("Lỗi / Triệu chứng", { width: widths[0], bold: true, shading: "D63031", color: "FFFFFF" }), cell("Nguyên nhân", { width: widths[1], bold: true, shading: "D63031", color: "FFFFFF" }), cell("Cách xử lý", { width: widths[2], bold: true, shading: "D63031", color: "FFFFFF" }), ]}), ...rows.map(r => new TableRow({ children: [ cell(r[0], { width: widths[0], bold: true }), cell(r[1], { width: widths[1] }), cell(r[2], { width: widths[2] }), ]})), ], }); }; // ===== Bảng tổng quát N cột ===== const genericTable = (headers, rows, widths, headerColor = BRAND) => { return new Table({ width: { size: CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: widths, rows: [ new TableRow({ tableHeader: true, children: headers.map((h, i) => cell(h, { width: widths[i], bold: true, shading: headerColor, color: "FFFFFF" }) )}), ...rows.map(r => new TableRow({ children: r.map((c, i) => cell(typeof c === 'string' ? c : c.text, { width: widths[i], bold: typeof c === 'object' ? c.bold : (i === 0), }) )})), ], }); }; // ===== Title page ===== const titlePage = (subtitle, partTitle) => [ new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 1200, after: 300 }, children: [new TextRun({ text: "SOLUTION_ERP", font: FONT, size: 56, bold: true, color: BRAND })], }), new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 120 }, children: [new TextRun({ text: "Hướng dẫn Sử dụng - User Manual", font: FONT, size: 36, bold: true, color: "333333", })], }), new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 600 }, children: [new TextRun({ text: partTitle, font: FONT, size: 28, color: "666666", })], }), new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 1200 }, children: [new TextRun({ text: "Công ty TNHH Xây dựng Solutions | Hệ thống Quản lý Hợp đồng", font: FONT, size: 22, italics: true, color: "888888", })], }), new Paragraph({ children: [new TextRun("")], pageBreakBefore: true }), h1("Mục lục"), new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }), new Paragraph({ children: [new TextRun("")], pageBreakBefore: true }), ]; // ===== Document builder ===== const buildDoc = (title, headerLabel, contentChildren) => new Document({ creator: "SOLUTION_ERP", title, description: title, styles: { default: { document: { run: { font: FONT, size: 22 } } }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 32, bold: true, font: FONT, color: BRAND }, paragraph: { spacing: { before: 360, after: 200 }, outlineLevel: 0 } }, { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 26, bold: true, font: FONT, color: "333333" }, paragraph: { spacing: { before: 280, after: 160 }, outlineLevel: 1 } }, { id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 22, bold: true, font: FONT, color: "555555" }, paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 } }, ], }, numbering: { config: [ { reference: "bullets", levels: [ { level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }, { level: 1, format: LevelFormat.BULLET, text: "◦", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 1440, hanging: 360 } } } }, ] }, { reference: "numbers", levels: [ { level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }, { level: 1, format: LevelFormat.LOWER_LETTER, text: "%2)", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 1440, hanging: 360 } } } }, ] }, ], }, sections: [{ properties: { page: { size: { width: PAGE_WIDTH, height: PAGE_HEIGHT }, margin: { top: MARGIN, right: MARGIN, bottom: MARGIN, left: MARGIN }, }, }, headers: { default: new Header({ children: [new Paragraph({ alignment: AlignmentType.RIGHT, children: [new TextRun({ text: headerLabel, font: FONT, size: 18, color: "888888", })], })]}), }, footers: { default: new Footer({ children: [new Paragraph({ tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }], children: [ new TextRun({ text: "© Solutions | v1.0 | 2026-05-04", font: FONT, size: 18, color: "888888" }), new TextRun({ text: "\tTrang ", font: FONT, size: 18, color: "888888" }), new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18, color: "888888" }), new TextRun({ text: " / ", font: FONT, size: 18, color: "888888" }), new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18, color: "888888" }), ], })]}), }, children: contentChildren, }], }); module.exports = { // Constants PAGE_WIDTH, PAGE_HEIGHT, MARGIN, CONTENT_WIDTH, FONT, BRAND, // Helpers h1, h2, h3, p, bullet, num, note, warn, tip, code, mockScreenshot, cell, fieldTable, errorTable, genericTable, titlePage, buildDoc, // Re-export docx primitives Paragraph, TextRun, Packer, };