import express from "express"; import { Document, Packer, Paragraph, HeadingLevel, TextRun, AlignmentType, BorderStyle } from "docx"; import PptxGenJS from "pptxgenjs"; import ExcelJS from "exceljs"; import PDFDocument from "pdfkit"; const router = express.Router(); const PURPLE = "7C3AED"; const LIGHT = "F3E8FF"; function sendBuffer(res, buf, filename, mime) { res.setHeader("Content-Type", mime); res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`); res.send(buf); } /* ---- قراءة مرنة للمحتوى ---- */ // يحوّل أي شكل قد يرسله النموذج إلى مصفوفة فقرات/أقسام موحّدة function normalizeBlocks(content) { const blocks = []; if (!content) return blocks; if (typeof content === "string") { blocks.push({ type: "p", text: content }); return blocks; } if (content.heading) blocks.push({ type: "h", text: content.heading }); // paragraphs (content.paragraphs || []).forEach(p => blocks.push({ type: "p", text: asText(p) })); // sections: {title, body|content|text|bullets} (content.sections || []).forEach(s => { if (s.title) blocks.push({ type: "h", text: s.title }); const body = s.body || s.content || s.text; if (body) blocks.push({ type: "p", text: asText(body) }); (s.bullets || s.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) })); }); // bullets/points على المستوى الأعلى (content.bullets || content.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) })); return blocks; } function asText(v) { if (v == null) return ""; if (typeof v === "string") return v; if (Array.isArray(v)) return v.map(asText).join("\n"); if (typeof v === "object") return v.text || v.body || v.content || JSON.stringify(v); return String(v); } // للعروض: استخرج شرائح موحّدة {title, bullets[]} function normalizeSlides(content, fallbackTitle) { let slides = []; if (content?.slides?.length) { slides = content.slides.map(s => ({ title: s.title || s.heading || fallbackTitle || "", bullets: normalizeBullets(s), })); } else { // اصنع شرائح من sections أو paragraphs const secs = content?.sections || []; if (secs.length) { slides = secs.map(s => ({ title: s.title || "", bullets: normalizeBullets(s) })); } else { slides = [{ title: fallbackTitle || "Slide", bullets: (content?.paragraphs || []).map(asText) }]; } } return slides.filter(s => s.title || (s.bullets && s.bullets.length)); } function normalizeBullets(s) { if (Array.isArray(s.bullets)) return s.bullets.map(asText); if (Array.isArray(s.points)) return s.points.map(asText); const body = s.body || s.content || s.text; if (Array.isArray(body)) return body.map(asText); if (typeof body === "string") return body.split(/\n|•/).map(x => x.trim()).filter(Boolean); return []; } /* ---- WORD (تصميم جميل) ---- */ async function buildDocx(title, content) { const blocks = normalizeBlocks(content); const children = [ new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 300 }, border: { bottom: { color: PURPLE, space: 6, style: BorderStyle.SINGLE, size: 18 } }, children: [new TextRun({ text: title || "Document", bold: true, size: 48, color: PURPLE })], }), ]; for (const b of blocks) { if (b.type === "h") children.push(new Paragraph({ spacing: { before: 200, after: 100 }, children: [new TextRun({ text: b.text, bold: true, size: 30, color: PURPLE })] })); else if (b.type === "li") children.push(new Paragraph({ bullet: { level: 0 }, spacing: { after: 60 }, children: [new TextRun({ text: b.text, size: 24 })] })); else children.push(new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: b.text, size: 24 })] })); } if (blocks.length === 0) children.push(new Paragraph({ children: [new TextRun(asText(content))] })); const doc = new Document({ sections: [{ children }] }); return Packer.toBuffer(doc); } /* ---- POWERPOINT (قالب إبداعي + أنيميشن) ---- */ async function buildPptx(title, content) { const pptx = new PptxGenJS(); pptx.defineLayout({ name: "WIDE", width: 13.333, height: 7.5 }); pptx.layout = "WIDE"; // قالب رئيسي بخلفية متدرّجة وأشكال pptx.defineSlideMaster({ title: "GENISI", background: { color: "FFFFFF" }, objects: [ { rect: { x: 0, y: 0, w: 13.333, h: 0.25, fill: { color: PURPLE } } }, { rect: { x: 0, y: 7.25, w: 13.333, h: 0.25, fill: { color: "C084FC" } } }, ], }); // شريحة العنوان const cover = pptx.addSlide({ masterName: "GENISI" }); cover.background = { color: PURPLE }; cover.addShape(pptx.ShapeType.ellipse, { x: 10.5, y: -1.5, w: 4, h: 4, fill: { color: "9333EA" } }); cover.addShape(pptx.ShapeType.ellipse, { x: -1, y: 5, w: 3.5, h: 3.5, fill: { color: "A855F7" } }); cover.addText(title || "Presentation", { x: 0.8, y: 2.8, w: 11.7, h: 1.5, fontSize: 44, bold: true, color: "FFFFFF", align: "center" }); cover.addText("Genisi", { x: 0.8, y: 4.4, w: 11.7, h: 0.6, fontSize: 18, color: "E9D5FF", align: "center" }); const slides = normalizeSlides(content, title); slides.forEach((s, idx) => { const slide = pptx.addSlide({ masterName: "GENISI" }); // شكل زخرفي slide.addShape(pptx.ShapeType.roundRect, { x: 0.5, y: 0.5, w: 0.5, h: 0.9, fill: { color: PURPLE }, rectRadius: 0.1 }); slide.addText(s.title || `Slide ${idx + 1}`, { x: 1.2, y: 0.5, w: 11, h: 0.9, fontSize: 30, bold: true, color: PURPLE }); // نقاط مع أنيميشن (ظهور تدريجي عبر options) const bullets = (s.bullets || []).map(b => ({ text: b, options: { bullet: { code: "2022" }, fontSize: 20, color: "1F2937", paraSpaceAfter: 10 } })); if (bullets.length) { slide.addText(bullets, { x: 1.2, y: 1.7, w: 11, h: 5, valign: "top" }); } // أنيميشن: تأثير دخول للشريحة (transition) slide.transition = { type: "fade", duration: 0.6 }; slide.addText(`${idx + 1}`, { x: 12.4, y: 6.7, w: 0.7, h: 0.4, fontSize: 12, color: "9CA3AF", align: "right" }); }); const data = await pptx.write("nodebuffer"); return Buffer.isBuffer(data) ? data : Buffer.from(data); } /* ---- EXCEL (تصميم جميل) ---- */ async function buildXlsx(title, content) { const wb = new ExcelJS.Workbook(); wb.creator = "Genisi"; const sheets = content?.sheets?.length ? content.sheets : [{ name: title || "Sheet1", headers: content?.headers || [], rows: content?.rows || [] }]; sheets.forEach(sh => { const ws = wb.addWorksheet((sh.name || "Sheet").slice(0, 31)); if (sh.headers?.length) { const hr = ws.addRow(sh.headers); hr.eachCell(c => { c.font = { bold: true, color: { argb: "FFFFFFFF" } }; c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF7C3AED" } }; c.alignment = { horizontal: "center", vertical: "middle" }; c.border = { bottom: { style: "thin", color: { argb: "FFCCCCCC" } } }; }); } (sh.rows || []).forEach((r, i) => { const row = ws.addRow(r); if (i % 2) row.eachCell(c => (c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFF6F0FF" } })); }); ws.columns.forEach(c => (c.width = 22)); }); const buf = await wb.xlsx.writeBuffer(); return Buffer.from(buf); } function buildCsv(content) { const headers = content?.headers || content?.sheets?.[0]?.headers || []; const rows = content?.rows || content?.sheets?.[0]?.rows || []; const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`; const lines = []; if (headers.length) lines.push(headers.map(esc).join(",")); rows.forEach(r => lines.push(r.map(esc).join(","))); return Buffer.from("\uFEFF" + lines.join("\n"), "utf8"); } /* ---- PDF (تصميم جميل) ---- */ function buildPdf(title, content) { return new Promise((resolve, reject) => { const doc = new PDFDocument({ margin: 54, size: "A4" }); const chunks = []; doc.on("data", c => chunks.push(c)); doc.on("end", () => resolve(Buffer.concat(chunks))); doc.on("error", reject); // شريط علوي doc.rect(0, 0, doc.page.width, 70).fill("#7C3AED"); doc.fillColor("#fff").fontSize(24).text(title || "Document", 54, 26, { width: doc.page.width - 108 }); doc.moveDown(2).fillColor("#000"); const blocks = normalizeBlocks(content); if (blocks.length === 0) { doc.fontSize(12).text(asText(content)); doc.end(); return; } let y = 100; doc.y = y; for (const b of blocks) { if (b.type === "h") { doc.moveDown(0.6).fontSize(15).fillColor("#7C3AED").text(b.text); doc.fillColor("#000"); } else if (b.type === "li") { doc.fontSize(12).fillColor("#000").text("• " + b.text, { indent: 12 }); } else { doc.fontSize(12).fillColor("#000").text(b.text); } doc.moveDown(0.3); } doc.end(); }); } router.post("/document", async (req, res) => { try { const { format = "pdf", title = "Genisi Document", content = {} } = req.body; const safe = (title || "document").replace(/[^\w\u0600-\u06FF\- ]/g, "_").slice(0, 60); switch (format) { case "docx": return sendBuffer(res, await buildDocx(title, content), `${safe}.docx`, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); case "pptx": return sendBuffer(res, await buildPptx(title, content), `${safe}.pptx`, "application/vnd.openxmlformats-officedocument.presentationml.presentation"); case "xlsx": return sendBuffer(res, await buildXlsx(title, content), `${safe}.xlsx`, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); case "csv": return sendBuffer(res, buildCsv(content), `${safe}.csv`, "text/csv; charset=utf-8"); case "pdf": default: return sendBuffer(res, await buildPdf(title, content), `${safe}.pdf`, "application/pdf"); } } catch (err) { console.error("document error:", err); res.status(500).json({ error: "document_failed", message: String(err?.message || err) }); } }); export default router;