UNPKG

@tasolutions/express-core

Version:
354 lines (303 loc) 15.3 kB
const { exec } = require('child_process'); const JsBarcode = require('jsbarcode'); const { createCanvas } = require('canvas'); module.exports = { /** * Chuyển đổi file DOCX sang PDF bằng LibreOffice * @param {String} docxFilePath - Đường dẫn file DOCX tạm thời * @param {String} pdfFilePath - Đường dẫn save file PDF * @returns {Promise<String>} - Đường dẫn file PDF đã chuyển đổi */ convertDocxToPdf: (docxFilePath, pdfFilePath) => { return new Promise((resolve, reject) => { // Sử dụng LibreOffice trực tiếp thay vì unoconv const outputDir = require('path').dirname(pdfFilePath); const command = `soffice --headless --convert-to pdf --outdir "${outputDir}" "${docxFilePath}"`; console.log(`Executing command: ${command}`); exec(command, (error, stdout, stderr) => { if (error) { console.error(`Lỗi chuyển đổi PDF: ${error.message}`); console.error(`stderr: ${stderr}`); // Thử fallback với unoconv nếu LibreOffice không hoạt động console.log('Thử fallback với unoconv...'); const unoconvCommand = `unoconv -f pdf -o "${pdfFilePath}" "${docxFilePath}"`; exec(unoconvCommand, (unoconvError, unoconvStdout, unoconvStderr) => { if (unoconvError) { console.error(`Lỗi unoconv fallback: ${unoconvError.message}`); console.error(`unoconv stderr: ${unoconvStderr}`); return reject(unoconvError); } console.log(`Unoconv fallback thành công: ${unoconvStdout}`); resolve(pdfFilePath); }); return; } if (stderr) { console.error(`LibreOffice stderr: ${stderr}`); } console.log(`LibreOffice chuyển đổi thành công: ${stdout}`); // LibreOffice tạo file với tên gốc + .pdf, cần rename const originalPdfPath = docxFilePath.replace('.docx', '.pdf'); const fs = require('fs'); if (fs.existsSync(originalPdfPath)) { fs.renameSync(originalPdfPath, pdfFilePath); console.log(`Đã rename file từ ${originalPdfPath} sang ${pdfFilePath}`); } resolve(pdfFilePath); }); }); }, /** * Function backup đơn giản để đảm bảo các field cơ bản có mặt * @param {Object} data - Dữ liệu gốc * @returns {Object} - Dữ liệu đã được bổ sung field cơ bản */ ensureBasicFieldsPresent: (data) => { const result = { ...data }; // Thêm các field cơ bản thường gặp nếu chưa có const basicFields = [ 'name', 'email', 'phone', 'address', 'description', 'title', 'content', 'status', 'type', 'category', 'tags', 'notes', 'comments', 'start_date', 'end_date', 'created_date', 'updated_date', 'is_active', 'is_deleted', 'is_public', 'is_private' ]; for (const fieldName of basicFields) { if (!(fieldName in result)) { result[fieldName] = ""; } } return result; }, /** * Đảm bảo tất cả field schema của Collection đều có mặt trong data * @param {Object} data - Dữ liệu gốc * @param {Object} Collection - Model của Collection * @returns {Object} - Dữ liệu đã được bổ sung đầy đủ field */ ensureAllFieldsPresent: (data, Collection) => { const result = { ...data }; try { // Lấy schema của Collection const schema = Collection.schema.obj; // Thêm tất cả field từ schema vào data nếu chưa có for (const [fieldName, fieldSchema] of Object.entries(schema)) { if (!(fieldName in result)) { // Xử lý an toàn cho tất cả loại field try { // Kiểm tra loại field để xử lý phù hợp if (fieldSchema && typeof fieldSchema === 'object') { // Nếu là ObjectId ref hoặc schema phức tạp if (fieldSchema.type === 'ObjectId' || fieldSchema.ref) { result[fieldName] = ""; // ObjectId ref - để rỗng } else if (fieldSchema.type === 'Array') { result[fieldName] = []; // Array - để mảng rỗng } else if (fieldSchema.type === 'Boolean') { result[fieldName] = false; // Boolean - để false } else if (fieldSchema.type === 'Number') { result[fieldName] = 0; // Number - để 0 } else if (fieldSchema.type === 'Date') { result[fieldName] = ""; // Date - để rỗng } else { result[fieldName] = ""; // Mặc định - để rỗng } } else { // Nếu là string đơn giản (ví dụ: String, Number, Boolean) result[fieldName] = ""; } } catch (fieldError) { // Nếu có lỗi khi xử lý field cụ thể, để rỗng console.warn(`[WARNING] Lỗi xử lý field ${fieldName}: ${fieldError.message}`); result[fieldName] = ""; } } } // Thêm các field đặc biệt của MongoDB nếu chưa có const specialFields = ['_id', 'created_at', 'updated_at', 'is_deleted', 'is_active']; for (const fieldName of specialFields) { if (!(fieldName in result)) { result[fieldName] = ""; } } console.log(`[DEBUG] Đã đảm bảo tất cả field schema có mặt. Tổng số field: ${Object.keys(result).length}`); } catch (error) { console.warn(`[WARNING] Không thể lấy schema của Collection: ${error.message}`); // Nếu không lấy được schema, vẫn trả về data gốc } return result; }, /** * Xử lý dữ liệu để đảm bảo không có field undefined/null khi render template * @param {Object} data - Dữ liệu gốc * @returns {Object} - Dữ liệu đã được xử lý */ sanitizeDataForTemplate: (data) => { const result = { ...data }; for (const [key, value] of Object.entries(result)) { // Xử lý các giá trị null, undefined, NaN if (value === null || value === undefined || (typeof value === 'number' && isNaN(value))) { result[key] = ""; } // Convert các giá trị khác thành string else if (typeof value !== 'string') { result[key] = String(value); } } return result; }, flattenObject: (obj, parentKey = '', separator = '__') => { const result = {}; for (let [key, value] of Object.entries(obj)) { const newKey = parentKey ? `${parentKey}${separator}${key}` : key; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { Object.assign(result, module.exports.flattenObject(value, newKey, separator)); } else { result[newKey] = value; // Kiểm tra nếu key là gender và dịch giá trị if (key === 'gender' && genderMapping[value]) { result[`${newKey}_vi`] = genderMapping[value]; } } } return result; }, calculateYearDifferenceForFlattenedData: (data, separator = '_') => { const result = { ...data }; // Sao chép dữ liệu gốc để tránh thay đổi trực tiếp // Xử lý các giá trị null hoặc undefined trước khi tính toán for (const key in result) { if (result[key] === null || result[key] === undefined || result[key] === '') { result[key] = ""; } } for (const [key, value] of Object.entries(result)) { // Chỉ xử lý nếu value không rỗng và có thể parse thành Date if (value && value !== "" && typeof value === 'string' && !isNaN(Date.parse(value))) { // Nếu giá trị là ngày tháng hợp lệ, tính khoảng cách năm const date = new Date(value); const currentDate = new Date(); const yearDifference = currentDate.getFullYear() - date.getFullYear(); const monthDiff = currentDate.getMonth() - date.getMonth(); const dayDiff = currentDate.getDate() - date.getDate(); // Điều chỉnh nếu tháng hoặc ngày chưa đến const adjustedYearDifference = monthDiff < 0 || (monthDiff === 0 && dayDiff < 0) ? yearDifference - 1 : yearDifference; // Thêm trường mới với hậu tố `__year` - convert thành string result[`${key}${separator}year`] = currentDate.getFullYear().toString(); // Thêm trường mới với hậu tố `__month` - convert thành string result[`${key}${separator}month`] = (currentDate.getMonth() + 1).toString(); // +1 vì getMonth() trả về 0-11 // Thêm trường mới với hậu tố `__day` - convert thành string result[`${key}${separator}day`] = currentDate.getDate().toString(); // Thêm trường mới với hậu tố `__year_diff` - convert thành string result[`${key}${separator}year_diff`] = adjustedYearDifference.toString(); // Thêm các field bổ sung cho date gốc - convert thành string result[`${key}${separator}original_year`] = date.getFullYear().toString(); result[`${key}${separator}original_month`] = (date.getMonth() + 1).toString(); result[`${key}${separator}original_day`] = date.getDate().toString(); } } return result; }, /** * Chuyển đổi base64 thành ArrayBuffer (hỗ trợ xử lý hình ảnh QR code và barcode) * @param {String} base64 - Chuỗi base64 của hình ảnh * @returns {ArrayBuffer} - ArrayBuffer để sử dụng trong docxtemplater */ base64ToArrayBuffer: (base64) => { var binaryString = Buffer.from(base64, 'base64').toString('binary'); var len = binaryString.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; }, /** * Tạo barcode từ chuỗi đầu vào * @param {String} text - Chuỗi cần tạo barcode * @param {Object} options - Tùy chọn cho barcode (loại, kích thước, v.v.) * @returns {String} - Chuỗi base64 của hình ảnh barcode */ generateBarcode: (text, options = {}) => { // Tạo canvas để vẽ barcode const canvas = createCanvas(300, 100); // Cấu hình mặc định cho barcode const defaultOptions = { format: 'CODE128', // Loại barcode width: 2, // Độ rộng của thanh height: 100, // Chiều cao của barcode displayValue: false, // Không hiển thị giá trị bên dưới barcode fontSize: 20, // Kích thước font chữ margin: 10, // Lề background: '#ffffff', // Màu nền lineColor: '#000000' // Màu thanh }; // Kết hợp tùy chọn mặc định với tùy chọn được cung cấp const barcodeOptions = { ...defaultOptions, ...options }; try { // Tạo barcode JsBarcode(canvas, text, barcodeOptions); // Chuyển đổi canvas thành chuỗi base64 return canvas.toDataURL('image/png'); } catch (error) { console.error(`Lỗi khi tạo barcode: ${error.message}`); // Trả về một hình ảnh lỗi hoặc null return null; } }, /** * Lấy danh sách field boolean từ Collection schema hoặc từ data */ getBooleanFields: (data, Collection) => { let fields = []; if (Collection && Collection.schema && Collection.schema.obj) { for (const [field, schema] of Object.entries(Collection.schema.obj)) { if (schema === Boolean || (schema && schema.type === Boolean)) { fields.push(field); } } } // Nếu không có Collection hoặc không tìm thấy, lấy từ data if (fields.length === 0) { fields = Object.keys(data).filter(key => typeof data[key] === 'boolean'); } return fields; }, /** * Tự động sinh field tick_yes/tick_no cho tất cả field boolean hoặc field không có giá trị * @param {Object} data - Dữ liệu đã flatten/sanitize * @param {Object} [Collection] - Model của Collection (tùy chọn, để lấy danh sách field boolean nếu cần) * @returns {Object} - Dữ liệu đã bổ sung field tick */ addBooleanTicks: (data, Collection) => { const result = { ...data }; const booleanFields = module.exports.getBooleanFields(data, Collection); for (const field of booleanFields) { const value = data[field]; if (typeof value === 'boolean') { result[`tick_${field}_yes`] = value ? "☑" : "☐"; result[`tick_${field}_no`] = value ? "☐" : "☑"; } else { // Nếu không có giá trị, cả hai đều là tick trống result[`tick_${field}_yes`] = "☐"; result[`tick_${field}_no`] = "☐"; } } return result; }, /** * Chuyển các field string 'true'/'false' về boolean thực sự * @param {Object} data * @returns {Object} */ convertStringBooleans: (data) => { const result = { ...data }; for (const [key, value] of Object.entries(result)) { if (value === "true") result[key] = true; else if (value === "false") result[key] = false; } return result; } } const genderMapping = { MALE: "Nam", FEMALE: "Nữ", OTHER: "Khác" };