diff --git a/competitor-analysis/services/pdfService.ts b/competitor-analysis/services/pdfService.ts new file mode 100644 index 00000000..3c9b6887 --- /dev/null +++ b/competitor-analysis/services/pdfService.ts @@ -0,0 +1,179 @@ +import type { AppState } from '../types'; +// @ts-ignore +import { jsPDF } from "jspdf"; +import "jspdf-autotable"; + +const addWrappedText = (doc: any, text: string, x: number, y: number, maxWidth: number): number => { + const lines = doc.splitTextToSize(text, maxWidth); + doc.text(lines, x, y); + // Approximate height of the text block based on font size and line height + const fontSize = doc.getFontSize(); + const lineHeight = 1.15; + return lines.length * fontSize * lineHeight / 2.8; +}; + +export const generatePdfReport = async (appState: AppState, t: any) => { + const doc = new jsPDF(); + const { company, analyses, silver_bullets, conclusion, battlecards, reference_analysis } = appState; + const pageMargin = 15; + const pageWidth = doc.internal.pageSize.getWidth(); + const contentWidth = pageWidth - 2 * pageMargin; + let yPos = 20; + + // Title Page + doc.setFontSize(22); + doc.setFont('helvetica', 'bold'); + doc.text(t.title, pageWidth / 2, yPos, { align: 'center' }); + yPos += 10; + doc.setFontSize(16); + doc.setFont('helvetica', 'normal'); + doc.text(company.name, pageWidth / 2, yPos, { align: 'center' }); + yPos += 15; + + const addPageIfNeeded = (requiredHeight: number) => { + if (yPos + requiredHeight > 280) { // 297mm height, 10mm margin bottom + doc.addPage(); + yPos = 20; + } + }; + + const addHeader = (title: string) => { + addPageIfNeeded(20); + doc.setFontSize(14); + doc.setFont('helvetica', 'bold'); + doc.text(title, pageMargin, yPos); + yPos += 8; + doc.setFont('helvetica', 'normal'); + doc.setFontSize(10); + }; + + // Summary & Opportunities + if (conclusion) { + addHeader(t.summary); + yPos += addWrappedText(doc, conclusion.summary, pageMargin, yPos, contentWidth); + yPos += 5; + + addHeader(t.opportunities); + yPos += addWrappedText(doc, conclusion.opportunities, pageMargin, yPos, contentWidth); + yPos += 10; + } + + // Analysis Details + addHeader(t.step4_title); + analyses.forEach(a => { + addPageIfNeeded(40); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(a.competitor.name, pageMargin, yPos); + yPos += 6; + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + + yPos += addWrappedText(doc, `Portfolio: ${a.portfolio.map(p => p.product).join(', ')}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += addWrappedText(doc, `Industries: ${a.target_industries.join(', ')}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += addWrappedText(doc, `Differentiators: ${a.differentiators.join('; ')}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += 5; + }); + + // Silver Bullets + addHeader(t.step5_title); + silver_bullets.forEach(b => { + addPageIfNeeded(15); + yPos += addWrappedText(doc, `${t.against} ${b.competitor_name}: "${b.statement}"`, pageMargin, yPos, contentWidth); + yPos += 2; + }); + yPos += 5; + + // Matrices + if (conclusion) { + const transformMatrix = (matrix: any[] | undefined, rowKeyName: 'product' | 'industry') => { + if (!matrix || matrix.length === 0) return { head: [], body: [] }; + const allCompetitors = new Set(); + matrix.forEach(row => (row.availability || []).forEach((item: any) => allCompetitors.add(item.competitor))); + const head = [['', ...Array.from(allCompetitors).sort()]]; + const body = matrix.map(row => { + const rowData: (string|any)[] = [row[rowKeyName]]; + head[0].slice(1).forEach(competitor => { + const item = (row.availability || []).find((a: any) => a.competitor === competitor); + rowData.push(item && item.has_offering ? '✓' : ''); + }); + return rowData; + }); + return { head, body }; + }; + + if (conclusion.product_matrix) { + addPageIfNeeded(50); + addHeader(t.productMatrix); + const { head, body } = transformMatrix(conclusion.product_matrix, 'product'); + (doc as any).autoTable({ + head: head, body: body, startY: yPos, theme: 'striped', + headStyles: { fillColor: [65, 90, 119] }, margin: { left: pageMargin } + }); + yPos = (doc as any).lastAutoTable.finalY + 10; + } + + if (conclusion.industry_matrix) { + addPageIfNeeded(50); + addHeader(t.industryMatrix); + const { head, body } = transformMatrix(conclusion.industry_matrix, 'industry'); + (doc as any).autoTable({ + head: head, body: body, startY: yPos, theme: 'striped', + headStyles: { fillColor: [65, 90, 119] }, margin: { left: pageMargin } + }); + yPos = (doc as any).lastAutoTable.finalY + 10; + } + } + + // Battlecards + if (battlecards && battlecards.length > 0) { + addHeader(t.step7_title); + battlecards.forEach(card => { + addPageIfNeeded(60); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(`${t.against} ${card.competitor_name}`, pageMargin, yPos); + yPos += 6; + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + yPos += addWrappedText(doc, `${t.focus}: ${card.competitor_profile.focus}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += addWrappedText(doc, `${t.strengthsVsWeaknesses}:\n - ${(card.strengths_vs_weaknesses || []).join('\n - ')}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += addWrappedText(doc, `${t.landmineQuestions}:\n - ${(card.landmine_questions || []).join('\n - ')}`, pageMargin + 5, yPos, contentWidth - 5); + yPos += addWrappedText(doc, `${t.silverBullet}: "${card.silver_bullet}"`, pageMargin + 5, yPos, contentWidth - 5); + yPos += 8; + }); + } + + // References + if (reference_analysis && reference_analysis.length > 0) { + addPageIfNeeded(50); + addHeader(t.step8_title); + reference_analysis.forEach(analysis => { + addPageIfNeeded(30); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(analysis.competitor_name, pageMargin, yPos); + yPos += 6; + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + if ((analysis.references || []).length > 0) { + (analysis.references || []).forEach(ref => { + yPos += addWrappedText(doc, `${ref.name} (${ref.industry || 'N/A'}): "${ref.testimonial_snippet || ''}"`, pageMargin + 5, yPos, contentWidth - 5); + }); + } else { + yPos += addWrappedText(doc, t.noReferencesFound, pageMargin + 5, yPos, contentWidth - 5); + } + yPos += 5; + }); + } + + // Add footer with page numbers + const pageCount = (doc as any).internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.text(`Page ${i} of ${pageCount}`, pageWidth - pageMargin, doc.internal.pageSize.getHeight() - 10, { align: 'right' }); + } + + doc.save(`analysis_${company.name}.pdf`); +}; \ No newline at end of file