@mathrunet/masamune_firebase_workflow_marketing 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +361 -0
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/assets/fonts/NotoSans-Bold.ttf +1439 -0
- package/assets/fonts/NotoSans-Regular.ttf +1439 -0
- package/assets/fonts/NotoSans.ttf +0 -0
- package/assets/fonts/NotoSansJP-Bold.ttf +0 -0
- package/assets/fonts/NotoSansJP-Regular.ttf +0 -0
- package/assets/fonts/NotoSansKR-Bold.ttf +1439 -0
- package/assets/fonts/NotoSansKR-Regular.ttf +1439 -0
- package/assets/fonts/NotoSansKR.ttf +0 -0
- package/assets/fonts/NotoSansSC-Bold.ttf +1439 -0
- package/assets/fonts/NotoSansSC-Regular.ttf +1439 -0
- package/assets/fonts/NotoSansSC.ttf +0 -0
- package/dist/clients/app_store_client.d.ts +54 -0
- package/dist/clients/app_store_client.js +280 -0
- package/dist/clients/app_store_client.js.map +1 -0
- package/dist/clients/firebase_analytics_client.d.ts +67 -0
- package/dist/clients/firebase_analytics_client.js +411 -0
- package/dist/clients/firebase_analytics_client.js.map +1 -0
- package/dist/clients/github_content_client.d.ts +83 -0
- package/dist/clients/github_content_client.js +443 -0
- package/dist/clients/github_content_client.js.map +1 -0
- package/dist/clients/google_play_client.d.ts +49 -0
- package/dist/clients/google_play_client.js +275 -0
- package/dist/clients/google_play_client.js.map +1 -0
- package/dist/functions/analyze_github_init.d.ts +37 -0
- package/dist/functions/analyze_github_init.js +299 -0
- package/dist/functions/analyze_github_init.js.map +1 -0
- package/dist/functions/analyze_github_process.d.ts +25 -0
- package/dist/functions/analyze_github_process.js +268 -0
- package/dist/functions/analyze_github_process.js.map +1 -0
- package/dist/functions/analyze_github_summary.d.ts +32 -0
- package/dist/functions/analyze_github_summary.js +270 -0
- package/dist/functions/analyze_github_summary.js.map +1 -0
- package/dist/functions/analyze_market_research.d.ts +40 -0
- package/dist/functions/analyze_market_research.js +346 -0
- package/dist/functions/analyze_market_research.js.map +1 -0
- package/dist/functions/analyze_marketing_data.d.ts +96 -0
- package/dist/functions/analyze_marketing_data.js +990 -0
- package/dist/functions/analyze_marketing_data.js.map +1 -0
- package/dist/functions/collect_from_app_store.d.ts +25 -0
- package/dist/functions/collect_from_app_store.js +148 -0
- package/dist/functions/collect_from_app_store.js.map +1 -0
- package/dist/functions/collect_from_firebase_analytics.d.ts +25 -0
- package/dist/functions/collect_from_firebase_analytics.js +142 -0
- package/dist/functions/collect_from_firebase_analytics.js.map +1 -0
- package/dist/functions/collect_from_google_play_console.d.ts +25 -0
- package/dist/functions/collect_from_google_play_console.js +142 -0
- package/dist/functions/collect_from_google_play_console.js.map +1 -0
- package/dist/functions/generate_marketing_markdown.d.ts +89 -0
- package/dist/functions/generate_marketing_markdown.js +653 -0
- package/dist/functions/generate_marketing_markdown.js.map +1 -0
- package/dist/functions/generate_marketing_pdf.d.ts +25 -0
- package/dist/functions/generate_marketing_pdf.js +175 -0
- package/dist/functions/generate_marketing_pdf.js.map +1 -0
- package/dist/functions/research_market.d.ts +56 -0
- package/dist/functions/research_market.js +434 -0
- package/dist/functions/research_market.js.map +1 -0
- package/dist/functions.d.ts +571 -0
- package/dist/functions.js +648 -0
- package/dist/functions.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/de_DE.d.ts +5 -0
- package/dist/locales/de_DE.js +111 -0
- package/dist/locales/de_DE.js.map +1 -0
- package/dist/locales/en.d.ts +5 -0
- package/dist/locales/en.js +111 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/locales/es_ES.d.ts +5 -0
- package/dist/locales/es_ES.js +111 -0
- package/dist/locales/es_ES.js.map +1 -0
- package/dist/locales/fr_FR.d.ts +5 -0
- package/dist/locales/fr_FR.js +111 -0
- package/dist/locales/fr_FR.js.map +1 -0
- package/dist/locales/id_ID.d.ts +5 -0
- package/dist/locales/id_ID.js +111 -0
- package/dist/locales/id_ID.js.map +1 -0
- package/dist/locales/index.d.ts +59 -0
- package/dist/locales/index.js +155 -0
- package/dist/locales/index.js.map +1 -0
- package/dist/locales/ja.d.ts +5 -0
- package/dist/locales/ja.js +111 -0
- package/dist/locales/ja.js.map +1 -0
- package/dist/locales/ko_KR.d.ts +5 -0
- package/dist/locales/ko_KR.js +111 -0
- package/dist/locales/ko_KR.js.map +1 -0
- package/dist/locales/pt_PT.d.ts +5 -0
- package/dist/locales/pt_PT.js +111 -0
- package/dist/locales/pt_PT.js.map +1 -0
- package/dist/locales/ru_RU.d.ts +5 -0
- package/dist/locales/ru_RU.js +111 -0
- package/dist/locales/ru_RU.js.map +1 -0
- package/dist/locales/types.d.ts +105 -0
- package/dist/locales/types.js +8 -0
- package/dist/locales/types.js.map +1 -0
- package/dist/locales/zh_CN.d.ts +5 -0
- package/dist/locales/zh_CN.js +111 -0
- package/dist/locales/zh_CN.js.map +1 -0
- package/dist/models/app_store_data.d.ts +87 -0
- package/dist/models/app_store_data.js +8 -0
- package/dist/models/app_store_data.js.map +1 -0
- package/dist/models/chart_data.d.ts +104 -0
- package/dist/models/chart_data.js +8 -0
- package/dist/models/chart_data.js.map +1 -0
- package/dist/models/common.d.ts +42 -0
- package/dist/models/common.js +8 -0
- package/dist/models/common.js.map +1 -0
- package/dist/models/firebase_analytics_data.d.ts +134 -0
- package/dist/models/firebase_analytics_data.js +8 -0
- package/dist/models/firebase_analytics_data.js.map +1 -0
- package/dist/models/github_analysis_data.d.ts +266 -0
- package/dist/models/github_analysis_data.js +101 -0
- package/dist/models/github_analysis_data.js.map +1 -0
- package/dist/models/google_play_data.d.ts +115 -0
- package/dist/models/google_play_data.js +8 -0
- package/dist/models/google_play_data.js.map +1 -0
- package/dist/models/index.d.ts +14 -0
- package/dist/models/index.js +36 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/market_research_data.d.ts +259 -0
- package/dist/models/market_research_data.js +8 -0
- package/dist/models/market_research_data.js.map +1 -0
- package/dist/models/marketing_analysis_data.d.ts +200 -0
- package/dist/models/marketing_analysis_data.js +8 -0
- package/dist/models/marketing_analysis_data.js.map +1 -0
- package/dist/models/pdf_data.d.ts +112 -0
- package/dist/models/pdf_data.js +8 -0
- package/dist/models/pdf_data.js.map +1 -0
- package/dist/services/chart_service.d.ts +54 -0
- package/dist/services/chart_service.js +436 -0
- package/dist/services/chart_service.js.map +1 -0
- package/dist/services/github_analysis_service.d.ts +73 -0
- package/dist/services/github_analysis_service.js +488 -0
- package/dist/services/github_analysis_service.js.map +1 -0
- package/dist/services/pdf_service.d.ts +112 -0
- package/dist/services/pdf_service.js +1397 -0
- package/dist/services/pdf_service.js.map +1 -0
- package/dist/utils/github_analysis_storage.d.ts +88 -0
- package/dist/utils/github_analysis_storage.js +154 -0
- package/dist/utils/github_analysis_storage.js.map +1 -0
- package/package.json +64 -0
- package/scripts/cleanup_test_data.ts +304 -0
- package/scripts/list_test_data.ts +85 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PDF Service
|
|
4
|
+
*
|
|
5
|
+
* Generates PDF marketing reports using pdfkit.
|
|
6
|
+
* Supports multilingual text with appropriate fonts.
|
|
7
|
+
*
|
|
8
|
+
* @see https://pdfkit.org/
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
44
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.PDFService = void 0;
|
|
48
|
+
const pdfkit_1 = __importDefault(require("pdfkit"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const locales_1 = require("../locales");
|
|
52
|
+
/**
|
|
53
|
+
* Font family configurations for multilingual support.
|
|
54
|
+
*/
|
|
55
|
+
const FONT_CONFIGS = [
|
|
56
|
+
{ name: "NotoSansJP", regularFile: "NotoSansJP-Regular.ttf", boldFile: "NotoSansJP-Bold.ttf" },
|
|
57
|
+
{ name: "NotoSansSC", regularFile: "NotoSansSC.ttf", boldFile: "NotoSansSC.ttf", isVariable: true },
|
|
58
|
+
{ name: "NotoSansKR", regularFile: "NotoSansKR.ttf", boldFile: "NotoSansKR.ttf", isVariable: true },
|
|
59
|
+
{ name: "NotoSans", regularFile: "NotoSans.ttf", boldFile: "NotoSans.ttf", isVariable: true },
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Light color scheme (default).
|
|
63
|
+
*/
|
|
64
|
+
const LIGHT_SCHEME = {
|
|
65
|
+
background: "#ffffff",
|
|
66
|
+
text: "#000000",
|
|
67
|
+
textSecondary: "#757575",
|
|
68
|
+
primary: "#1565c0",
|
|
69
|
+
primaryLight: "#e3f2fd",
|
|
70
|
+
success: "#2e7d32",
|
|
71
|
+
successLight: "#e8f5e9",
|
|
72
|
+
successDark: "#1b5e20",
|
|
73
|
+
error: "#c62828",
|
|
74
|
+
errorLight: "#ffebee",
|
|
75
|
+
errorDark: "#b71c1c",
|
|
76
|
+
warning: "#f57c00",
|
|
77
|
+
neutral: "#757575",
|
|
78
|
+
googlePlay: "#4CAF50",
|
|
79
|
+
appStore: "#007AFF",
|
|
80
|
+
boxBackground: "#f5f5f5",
|
|
81
|
+
summaryBackground: "#e3f2fd",
|
|
82
|
+
highlightBackground: "#e8f5e9",
|
|
83
|
+
concernBackground: "#ffebee",
|
|
84
|
+
competitorHeader: "#37474f",
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Dark color scheme.
|
|
88
|
+
*/
|
|
89
|
+
const DARK_SCHEME = {
|
|
90
|
+
background: "#212121",
|
|
91
|
+
text: "#ffffff",
|
|
92
|
+
textSecondary: "#b0b0b0",
|
|
93
|
+
primary: "#64b5f6",
|
|
94
|
+
primaryLight: "#1e3a5f",
|
|
95
|
+
success: "#81c784",
|
|
96
|
+
successLight: "#1b3d1e",
|
|
97
|
+
successDark: "#4caf50",
|
|
98
|
+
error: "#ef5350",
|
|
99
|
+
errorLight: "#3d1b1b",
|
|
100
|
+
errorDark: "#f44336",
|
|
101
|
+
warning: "#ffb74d",
|
|
102
|
+
neutral: "#9e9e9e",
|
|
103
|
+
googlePlay: "#66bb6a",
|
|
104
|
+
appStore: "#42a5f5",
|
|
105
|
+
boxBackground: "#1e1e1e",
|
|
106
|
+
summaryBackground: "#1e3a5f",
|
|
107
|
+
highlightBackground: "#1b3d1e",
|
|
108
|
+
concernBackground: "#3d1b1b",
|
|
109
|
+
competitorHeader: "#263238",
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* PDF Service for generating marketing reports.
|
|
113
|
+
*/
|
|
114
|
+
class PDFService {
|
|
115
|
+
pageWidth = 595.28; // A4 width in points
|
|
116
|
+
pageHeight = 841.89; // A4 height in points
|
|
117
|
+
margin = 50;
|
|
118
|
+
headerHeight = 45; // Header height in points
|
|
119
|
+
footerHeight = 30; // Footer height in points
|
|
120
|
+
contentWidth;
|
|
121
|
+
maxContentY; // Maximum Y position for content (before footer)
|
|
122
|
+
fontDir;
|
|
123
|
+
registeredFonts = new Set();
|
|
124
|
+
currentFontFamily = "Helvetica";
|
|
125
|
+
translations;
|
|
126
|
+
colors = LIGHT_SCHEME;
|
|
127
|
+
styleOptions = {};
|
|
128
|
+
headerIconBuffer = null;
|
|
129
|
+
constructor() {
|
|
130
|
+
this.contentWidth = this.pageWidth - this.margin * 2;
|
|
131
|
+
// Maximum Y for content: page height - bottom margin - footer height
|
|
132
|
+
this.maxContentY = this.pageHeight - this.margin - this.footerHeight;
|
|
133
|
+
// Default font directory: assets/fonts relative to package root
|
|
134
|
+
this.fontDir = path.join(__dirname, "..", "..", "assets", "fonts");
|
|
135
|
+
// Default to English translations
|
|
136
|
+
this.translations = (0, locales_1.getTranslations)("en");
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Download image from URL and return as Buffer.
|
|
140
|
+
*/
|
|
141
|
+
async downloadImage(url) {
|
|
142
|
+
try {
|
|
143
|
+
// Convert GitHub blob URL to raw URL
|
|
144
|
+
let rawUrl = url;
|
|
145
|
+
if (url.includes("github.com") && url.includes("/blob/")) {
|
|
146
|
+
rawUrl = url
|
|
147
|
+
.replace("github.com", "raw.githubusercontent.com")
|
|
148
|
+
.replace("/blob/", "/");
|
|
149
|
+
}
|
|
150
|
+
const response = await fetch(rawUrl);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
console.warn(`Failed to download image from ${rawUrl}: ${response.status}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
156
|
+
return Buffer.from(arrayBuffer);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.warn(`Error downloading image from ${url}:`, error);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Draw page background for dark mode.
|
|
165
|
+
*/
|
|
166
|
+
drawPageBackground(doc) {
|
|
167
|
+
if (this.styleOptions.colorScheme === "dark") {
|
|
168
|
+
doc.save();
|
|
169
|
+
doc.rect(0, 0, this.pageWidth, this.pageHeight).fill(this.colors.background);
|
|
170
|
+
doc.restore();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Draw page header with icon and organization title.
|
|
175
|
+
*/
|
|
176
|
+
drawPageHeader(doc) {
|
|
177
|
+
let y = this.margin;
|
|
178
|
+
// Only draw header if we have icon or organization title
|
|
179
|
+
if (!this.headerIconBuffer && !this.styleOptions.organizationTitle) {
|
|
180
|
+
return y;
|
|
181
|
+
}
|
|
182
|
+
// Draw icon on the left
|
|
183
|
+
if (this.headerIconBuffer) {
|
|
184
|
+
try {
|
|
185
|
+
const iconSize = 28;
|
|
186
|
+
doc.image(this.headerIconBuffer, this.margin, y, {
|
|
187
|
+
width: iconSize,
|
|
188
|
+
height: iconSize,
|
|
189
|
+
fit: [iconSize, iconSize],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.warn("Failed to draw header icon:", error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Draw organization title on the right
|
|
197
|
+
if (this.styleOptions.organizationTitle) {
|
|
198
|
+
doc.fontSize(11).font(this.getFont(true));
|
|
199
|
+
doc.fillColor(this.colors.text);
|
|
200
|
+
doc.text(this.styleOptions.organizationTitle, this.margin, y + 8, { width: this.contentWidth, align: "right" });
|
|
201
|
+
}
|
|
202
|
+
return y + this.headerHeight;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Draw page footer with copyright.
|
|
206
|
+
* Uses direct graphics commands to avoid auto-pagination.
|
|
207
|
+
*/
|
|
208
|
+
drawPageFooter(doc) {
|
|
209
|
+
if (!this.styleOptions.copyright) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const footerText = `© ${new Date().getFullYear()} ${this.styleOptions.copyright}`;
|
|
213
|
+
const footerY = this.pageHeight - this.footerHeight;
|
|
214
|
+
const fontSize = 8;
|
|
215
|
+
// Save current position
|
|
216
|
+
const savedY = doc.y;
|
|
217
|
+
// Set font and color
|
|
218
|
+
doc.fontSize(fontSize).font(this.getFont());
|
|
219
|
+
doc.fillColor(this.colors.textSecondary);
|
|
220
|
+
// Calculate text width for centering
|
|
221
|
+
const textWidth = doc.widthOfString(footerText);
|
|
222
|
+
const centerX = this.margin + (this.contentWidth - textWidth) / 2;
|
|
223
|
+
// Use save/restore and direct positioning to avoid auto-pagination
|
|
224
|
+
doc.save();
|
|
225
|
+
// Draw text using _fragment or simpleText approach
|
|
226
|
+
// Move to position and draw without triggering pagination
|
|
227
|
+
doc._fragment(footerText, centerX, footerY, {
|
|
228
|
+
lineBreak: false,
|
|
229
|
+
textWidth: textWidth,
|
|
230
|
+
wordSpacing: 0,
|
|
231
|
+
characterSpacing: 0,
|
|
232
|
+
});
|
|
233
|
+
doc.restore();
|
|
234
|
+
// Restore Y position to prevent state pollution
|
|
235
|
+
doc.y = savedY;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Initialize a new page with background, header, and return starting Y position.
|
|
239
|
+
* When isNewPage is true, draws footer on current page before adding new page.
|
|
240
|
+
*/
|
|
241
|
+
initPage(doc, isNewPage = false) {
|
|
242
|
+
if (isNewPage) {
|
|
243
|
+
// Draw footer on current page before creating new page
|
|
244
|
+
this.drawPageFooter(doc);
|
|
245
|
+
doc.addPage();
|
|
246
|
+
}
|
|
247
|
+
this.drawPageBackground(doc);
|
|
248
|
+
const y = this.drawPageHeader(doc);
|
|
249
|
+
return y;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Register fonts to PDFDocument based on locale.
|
|
253
|
+
*/
|
|
254
|
+
registerFonts(doc, locale) {
|
|
255
|
+
// Determine font family from locale
|
|
256
|
+
this.currentFontFamily = (0, locales_1.getFontFamily)(locale);
|
|
257
|
+
this.translations = (0, locales_1.getTranslations)(locale);
|
|
258
|
+
// Register all available font families
|
|
259
|
+
for (const config of FONT_CONFIGS) {
|
|
260
|
+
const regularPath = path.join(this.fontDir, config.regularFile);
|
|
261
|
+
const boldPath = path.join(this.fontDir, config.boldFile);
|
|
262
|
+
if (fs.existsSync(regularPath)) {
|
|
263
|
+
doc.registerFont(config.name, regularPath);
|
|
264
|
+
this.registeredFonts.add(config.name);
|
|
265
|
+
}
|
|
266
|
+
if (!config.isVariable && fs.existsSync(boldPath)) {
|
|
267
|
+
doc.registerFont(`${config.name}-Bold`, boldPath);
|
|
268
|
+
this.registeredFonts.add(`${config.name}-Bold`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get font name based on current locale.
|
|
274
|
+
*/
|
|
275
|
+
getFont(bold = false) {
|
|
276
|
+
if (this.currentFontFamily === "Helvetica") {
|
|
277
|
+
return bold ? "Helvetica-Bold" : "Helvetica";
|
|
278
|
+
}
|
|
279
|
+
// Check if the font was registered
|
|
280
|
+
if (this.registeredFonts.has(this.currentFontFamily)) {
|
|
281
|
+
// For variable fonts, bold is simulated or same font is used
|
|
282
|
+
if (bold && this.registeredFonts.has(`${this.currentFontFamily}-Bold`)) {
|
|
283
|
+
return `${this.currentFontFamily}-Bold`;
|
|
284
|
+
}
|
|
285
|
+
return this.currentFontFamily;
|
|
286
|
+
}
|
|
287
|
+
// Fallback to Helvetica
|
|
288
|
+
return bold ? "Helvetica-Bold" : "Helvetica";
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Generate a complete marketing report PDF.
|
|
292
|
+
*/
|
|
293
|
+
async generateReport(options) {
|
|
294
|
+
// Initialize style options
|
|
295
|
+
this.styleOptions = options.style || {};
|
|
296
|
+
this.colors = this.styleOptions.colorScheme === "dark" ? DARK_SCHEME : LIGHT_SCHEME;
|
|
297
|
+
// Download header icon if specified
|
|
298
|
+
if (this.styleOptions.headerIconUrl) {
|
|
299
|
+
this.headerIconBuffer = await this.downloadImage(this.styleOptions.headerIconUrl);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
this.headerIconBuffer = null;
|
|
303
|
+
}
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
try {
|
|
306
|
+
const appName = options.appName ||
|
|
307
|
+
options.data.appStore?.appName ||
|
|
308
|
+
options.data.googlePlayConsole?.packageName ||
|
|
309
|
+
"Marketing Report";
|
|
310
|
+
const doc = new pdfkit_1.default({
|
|
311
|
+
size: "A4",
|
|
312
|
+
margins: {
|
|
313
|
+
top: this.margin,
|
|
314
|
+
bottom: this.margin,
|
|
315
|
+
left: this.margin,
|
|
316
|
+
right: this.margin,
|
|
317
|
+
},
|
|
318
|
+
info: {
|
|
319
|
+
Title: `${appName} Marketing Report`,
|
|
320
|
+
Author: "Masamune Workflow Marketing",
|
|
321
|
+
Subject: options.dateRange
|
|
322
|
+
? `${options.dateRange.startDate} - ${options.dateRange.endDate}`
|
|
323
|
+
: new Date().toISOString().split("T")[0],
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
// Register fonts based on locale
|
|
327
|
+
const locale = typeof options.locale === "object"
|
|
328
|
+
? options.locale["@language"]
|
|
329
|
+
: options.locale;
|
|
330
|
+
this.registerFonts(doc, locale);
|
|
331
|
+
const chunks = [];
|
|
332
|
+
doc.on("data", (chunk) => chunks.push(chunk));
|
|
333
|
+
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
334
|
+
doc.on("error", reject);
|
|
335
|
+
// Draw background for first page (cover page)
|
|
336
|
+
this.drawPageBackground(doc);
|
|
337
|
+
// Cover page
|
|
338
|
+
this.addCoverPage(doc, options, appName);
|
|
339
|
+
// Executive Summary page
|
|
340
|
+
if (options.data.marketingAnalytics?.overallAnalysis) {
|
|
341
|
+
const summaryY = this.initPage(doc, true);
|
|
342
|
+
this.addSummaryPage(doc, options, summaryY);
|
|
343
|
+
// Highlights & Concerns page (separate page to avoid overflow)
|
|
344
|
+
const analysis = options.data.marketingAnalytics.overallAnalysis;
|
|
345
|
+
if ((analysis.highlights?.length || 0) > 0 || (analysis.concerns?.length || 0) > 0) {
|
|
346
|
+
const highlightsY = this.initPage(doc, true);
|
|
347
|
+
this.addHighlightsConcernsPage(doc, options, highlightsY);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// User Analytics page
|
|
351
|
+
if (options.data.firebaseAnalytics) {
|
|
352
|
+
const analyticsY = this.initPage(doc, true);
|
|
353
|
+
this.addUserAnalyticsPage(doc, options, analyticsY);
|
|
354
|
+
}
|
|
355
|
+
// Ratings & Reviews page
|
|
356
|
+
if (options.data.googlePlayConsole || options.data.appStore || options.data.marketingAnalytics?.reviewAnalysis) {
|
|
357
|
+
const ratingsY = this.initPage(doc, true);
|
|
358
|
+
this.addRatingsReviewsPage(doc, options, ratingsY);
|
|
359
|
+
}
|
|
360
|
+
// Competitive Positioning page (market research data)
|
|
361
|
+
if (options.data.marketingAnalytics?.competitivePositioning) {
|
|
362
|
+
const competitiveY = this.initPage(doc, true);
|
|
363
|
+
this.addCompetitivePositioningPage(doc, options, competitiveY);
|
|
364
|
+
}
|
|
365
|
+
// Market Opportunity Priority page (market research data)
|
|
366
|
+
if (options.data.marketingAnalytics?.marketOpportunityPriority) {
|
|
367
|
+
const opportunityY = this.initPage(doc, true);
|
|
368
|
+
this.addMarketOpportunityPriorityPage(doc, options, opportunityY);
|
|
369
|
+
}
|
|
370
|
+
// Trend Analysis page
|
|
371
|
+
if (options.data.marketingAnalytics?.trendAnalysis) {
|
|
372
|
+
const trendY = this.initPage(doc, true);
|
|
373
|
+
this.addTrendAnalysisPage(doc, options, trendY);
|
|
374
|
+
}
|
|
375
|
+
// Improvement Suggestions page
|
|
376
|
+
if (options.data.marketingAnalytics?.improvementSuggestions?.length) {
|
|
377
|
+
const improvementsY = this.initPage(doc, true);
|
|
378
|
+
this.addImprovementsPage(doc, options, improvementsY);
|
|
379
|
+
}
|
|
380
|
+
// GitHub-based Code Improvements page(s)
|
|
381
|
+
if (options.data.githubImprovements?.improvements?.length) {
|
|
382
|
+
const githubY = this.initPage(doc, true);
|
|
383
|
+
this.addGitHubImprovementsPage(doc, options, githubY);
|
|
384
|
+
}
|
|
385
|
+
// Draw footer on the last page
|
|
386
|
+
this.drawPageFooter(doc);
|
|
387
|
+
doc.end();
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
reject(error);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Add cover page to the PDF.
|
|
396
|
+
*/
|
|
397
|
+
addCoverPage(doc, options, appName) {
|
|
398
|
+
let y = 200;
|
|
399
|
+
// App name
|
|
400
|
+
doc.fontSize(32).font(this.getFont(true));
|
|
401
|
+
doc.fillColor(this.colors.text);
|
|
402
|
+
doc.text(appName, this.margin, y, {
|
|
403
|
+
width: this.contentWidth,
|
|
404
|
+
align: "center",
|
|
405
|
+
});
|
|
406
|
+
y += 60;
|
|
407
|
+
// Report type
|
|
408
|
+
doc.fontSize(20).font(this.getFont());
|
|
409
|
+
doc.fillColor(this.colors.text);
|
|
410
|
+
const t = this.translations;
|
|
411
|
+
const reportTypeLabel = options.reportType === "daily"
|
|
412
|
+
? t.dailyReport
|
|
413
|
+
: options.reportType === "weekly"
|
|
414
|
+
? t.weeklyReport
|
|
415
|
+
: t.monthlyReport;
|
|
416
|
+
doc.text(reportTypeLabel, this.margin, y, {
|
|
417
|
+
width: this.contentWidth,
|
|
418
|
+
align: "center",
|
|
419
|
+
});
|
|
420
|
+
y += 40;
|
|
421
|
+
// Date range
|
|
422
|
+
if (options.dateRange) {
|
|
423
|
+
doc.fontSize(16);
|
|
424
|
+
doc.fillColor(this.colors.text);
|
|
425
|
+
doc.text(`${options.dateRange.startDate} - ${options.dateRange.endDate}`, this.margin, y, { width: this.contentWidth, align: "center" });
|
|
426
|
+
y += 80;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
y += 40;
|
|
430
|
+
}
|
|
431
|
+
// Generated date
|
|
432
|
+
doc.fontSize(12).font(this.getFont());
|
|
433
|
+
doc.fillColor(this.colors.text);
|
|
434
|
+
doc.text(`${t.generated}: ${new Date().toISOString().split("T")[0]}`, this.margin, y, { width: this.contentWidth, align: "center" });
|
|
435
|
+
// Data sources at the bottom
|
|
436
|
+
y = this.pageHeight - 150;
|
|
437
|
+
doc.fontSize(10).font(this.getFont());
|
|
438
|
+
doc.fillColor(this.colors.textSecondary);
|
|
439
|
+
const sources = [];
|
|
440
|
+
if (options.data.googlePlayConsole)
|
|
441
|
+
sources.push("Google Play");
|
|
442
|
+
if (options.data.appStore)
|
|
443
|
+
sources.push("App Store");
|
|
444
|
+
if (options.data.firebaseAnalytics)
|
|
445
|
+
sources.push("Firebase Analytics");
|
|
446
|
+
if (options.data.marketingAnalytics)
|
|
447
|
+
sources.push(t.aiAnalysis);
|
|
448
|
+
doc.text(`${t.dataSources}: ${sources.join(" | ")}`, this.margin, y, {
|
|
449
|
+
width: this.contentWidth,
|
|
450
|
+
align: "center",
|
|
451
|
+
});
|
|
452
|
+
doc.fillColor(this.colors.text);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Add summary page with AI analysis.
|
|
456
|
+
*/
|
|
457
|
+
addSummaryPage(doc, options, startY = this.margin) {
|
|
458
|
+
let y = startY;
|
|
459
|
+
const analysis = options.data.marketingAnalytics?.overallAnalysis;
|
|
460
|
+
const t = this.translations;
|
|
461
|
+
// Page title
|
|
462
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
463
|
+
doc.text(t.executiveSummary, this.margin, y);
|
|
464
|
+
y += 35;
|
|
465
|
+
// Summary text
|
|
466
|
+
if (analysis?.summary) {
|
|
467
|
+
doc.fontSize(11).font(this.getFont());
|
|
468
|
+
doc.text(analysis.summary, this.margin, y, {
|
|
469
|
+
width: this.contentWidth,
|
|
470
|
+
align: "justify",
|
|
471
|
+
});
|
|
472
|
+
y = doc.y + 25;
|
|
473
|
+
}
|
|
474
|
+
// Engagement chart
|
|
475
|
+
if (options.charts?.engagement) {
|
|
476
|
+
try {
|
|
477
|
+
const chartWidth = 300;
|
|
478
|
+
const chartHeight = 200;
|
|
479
|
+
const chartX = this.margin + (this.contentWidth - chartWidth) / 2;
|
|
480
|
+
doc.image(options.charts.engagement, chartX, y, {
|
|
481
|
+
width: chartWidth,
|
|
482
|
+
height: chartHeight,
|
|
483
|
+
fit: [chartWidth, chartHeight],
|
|
484
|
+
});
|
|
485
|
+
y += chartHeight + 25;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Skip if image fails
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Key Metrics
|
|
492
|
+
if (analysis?.keyMetrics?.length) {
|
|
493
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
494
|
+
doc.text(t.metric, this.margin, y);
|
|
495
|
+
y += 20;
|
|
496
|
+
const metricsPerRow = 3;
|
|
497
|
+
const metricBoxWidth = this.contentWidth / metricsPerRow;
|
|
498
|
+
for (let i = 0; i < analysis.keyMetrics.length; i++) {
|
|
499
|
+
const metric = analysis.keyMetrics[i];
|
|
500
|
+
const col = i % metricsPerRow;
|
|
501
|
+
const x = this.margin + col * metricBoxWidth;
|
|
502
|
+
if (col === 0 && i > 0) {
|
|
503
|
+
y += 55;
|
|
504
|
+
}
|
|
505
|
+
// Metric box
|
|
506
|
+
doc.fillColor(this.colors.boxBackground);
|
|
507
|
+
doc.rect(x + 2, y, metricBoxWidth - 4, 50).fill();
|
|
508
|
+
// Metric title
|
|
509
|
+
doc.fillColor(this.colors.primary);
|
|
510
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
511
|
+
doc.text(metric.metric, x + 8, y + 8, {
|
|
512
|
+
width: metricBoxWidth - 16,
|
|
513
|
+
});
|
|
514
|
+
// Trend indicator and value
|
|
515
|
+
const trendColor = metric.trend === "up" ? this.colors.success : metric.trend === "down" ? this.colors.error : this.colors.textSecondary;
|
|
516
|
+
const trendIcon = metric.trend === "up" ? "+" : metric.trend === "down" ? "-" : "~";
|
|
517
|
+
doc.fillColor(trendColor);
|
|
518
|
+
doc.fontSize(13).font(this.getFont(true));
|
|
519
|
+
doc.text(`${metric.value} ${trendIcon}`, x + 8, y + 28, {
|
|
520
|
+
width: metricBoxWidth - 16,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
y += 70;
|
|
524
|
+
}
|
|
525
|
+
doc.fillColor(this.colors.text);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Add highlights and concerns page.
|
|
529
|
+
* This page displays highlights and concerns in a 2-column layout.
|
|
530
|
+
* Dynamically calculates item height and adds pages as needed.
|
|
531
|
+
*/
|
|
532
|
+
addHighlightsConcernsPage(doc, options, startY = this.margin) {
|
|
533
|
+
let y = startY;
|
|
534
|
+
const analysis = options.data.marketingAnalytics?.overallAnalysis;
|
|
535
|
+
const t = this.translations;
|
|
536
|
+
// Page title
|
|
537
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
538
|
+
doc.text(t.highlightsAndConcerns, this.margin, y);
|
|
539
|
+
y += 40;
|
|
540
|
+
const halfWidth = this.contentWidth / 2 - 15;
|
|
541
|
+
const boxPadding = 16; // Padding inside boxes
|
|
542
|
+
const itemSpacing = 10; // Space between items
|
|
543
|
+
// Get all items
|
|
544
|
+
const highlights = analysis?.highlights || [];
|
|
545
|
+
const concerns = analysis?.concerns || [];
|
|
546
|
+
// Headers
|
|
547
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
548
|
+
doc.fillColor(this.colors.successDark);
|
|
549
|
+
doc.text(t.highlights, this.margin, y);
|
|
550
|
+
doc.fillColor(this.colors.errorDark);
|
|
551
|
+
doc.text(t.concerns, this.margin + halfWidth + 30, y);
|
|
552
|
+
y += 25;
|
|
553
|
+
// Calculate heights for each item pair
|
|
554
|
+
doc.fontSize(10).font(this.getFont());
|
|
555
|
+
const maxItems = Math.max(highlights.length, concerns.length);
|
|
556
|
+
for (let i = 0; i < maxItems; i++) {
|
|
557
|
+
const highlightText = i < highlights.length ? highlights[i] : "";
|
|
558
|
+
const concernText = i < concerns.length ? concerns[i] : "";
|
|
559
|
+
// Calculate actual text heights
|
|
560
|
+
const highlightHeight = highlightText
|
|
561
|
+
? doc.heightOfString(highlightText, { width: halfWidth - boxPadding }) + boxPadding
|
|
562
|
+
: 0;
|
|
563
|
+
const concernHeight = concernText
|
|
564
|
+
? doc.heightOfString(concernText, { width: halfWidth - boxPadding }) + boxPadding
|
|
565
|
+
: 0;
|
|
566
|
+
// Use the larger height for this row
|
|
567
|
+
const rowHeight = Math.max(highlightHeight, concernHeight, 40);
|
|
568
|
+
// Check if we need a new page
|
|
569
|
+
if (y + rowHeight > this.maxContentY) {
|
|
570
|
+
this.drawPageFooter(doc);
|
|
571
|
+
doc.addPage();
|
|
572
|
+
this.drawPageBackground(doc);
|
|
573
|
+
y = this.drawPageHeader(doc);
|
|
574
|
+
// Re-add headers on new page
|
|
575
|
+
doc.fontSize(16).font(this.getFont(true));
|
|
576
|
+
doc.fillColor(this.colors.text);
|
|
577
|
+
doc.text(`${t.highlightsAndConcerns} ${t.continued}`, this.margin, y);
|
|
578
|
+
y += 30;
|
|
579
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
580
|
+
doc.fillColor(this.colors.successDark);
|
|
581
|
+
doc.text(t.highlights, this.margin, y);
|
|
582
|
+
doc.fillColor(this.colors.errorDark);
|
|
583
|
+
doc.text(t.concerns, this.margin + halfWidth + 30, y);
|
|
584
|
+
y += 25;
|
|
585
|
+
}
|
|
586
|
+
// Draw highlight item
|
|
587
|
+
if (highlightText) {
|
|
588
|
+
doc.fillColor(this.colors.highlightBackground);
|
|
589
|
+
doc.roundedRect(this.margin, y, halfWidth, rowHeight, 4).fill();
|
|
590
|
+
doc.fillColor(this.colors.successDark);
|
|
591
|
+
doc.fontSize(10).font(this.getFont());
|
|
592
|
+
doc.text(highlightText, this.margin + 8, y + 8, {
|
|
593
|
+
width: halfWidth - boxPadding,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// Draw concern item
|
|
597
|
+
if (concernText) {
|
|
598
|
+
doc.fillColor(this.colors.concernBackground);
|
|
599
|
+
doc.roundedRect(this.margin + halfWidth + 30, y, halfWidth, rowHeight, 4).fill();
|
|
600
|
+
doc.fillColor(this.colors.errorDark);
|
|
601
|
+
doc.fontSize(10).font(this.getFont());
|
|
602
|
+
doc.text(concernText, this.margin + halfWidth + 38, y + 8, {
|
|
603
|
+
width: halfWidth - boxPadding,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
y += rowHeight + itemSpacing;
|
|
607
|
+
}
|
|
608
|
+
doc.fillColor(this.colors.text);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Add user analytics page.
|
|
612
|
+
*/
|
|
613
|
+
addUserAnalyticsPage(doc, options, startY = this.margin) {
|
|
614
|
+
let y = startY;
|
|
615
|
+
const firebase = options.data.firebaseAnalytics;
|
|
616
|
+
const t = this.translations;
|
|
617
|
+
// Page title
|
|
618
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
619
|
+
doc.text(t.userAnalytics, this.margin, y);
|
|
620
|
+
y += 35;
|
|
621
|
+
if (firebase) {
|
|
622
|
+
// DAU/WAU/MAU stats
|
|
623
|
+
const dau = firebase.dau?.toLocaleString() || "N/A";
|
|
624
|
+
const wau = firebase.wau?.toLocaleString() || "N/A";
|
|
625
|
+
const mau = firebase.mau?.toLocaleString() || "N/A";
|
|
626
|
+
const newUsersVal = firebase.newUsers?.toLocaleString() || "N/A";
|
|
627
|
+
doc.fillColor(this.colors.primary);
|
|
628
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
629
|
+
doc.text(`DAU: ${dau} | WAU: ${wau} | MAU: ${mau} | ${t.newUsers}: ${newUsersVal}`, this.margin, y);
|
|
630
|
+
doc.fillColor(this.colors.text);
|
|
631
|
+
y += 25;
|
|
632
|
+
// Retention ratio
|
|
633
|
+
if (firebase.dau && firebase.mau && firebase.mau > 0) {
|
|
634
|
+
const retentionRatio = ((firebase.dau / firebase.mau) * 100).toFixed(1);
|
|
635
|
+
doc.fontSize(11).font(this.getFont());
|
|
636
|
+
doc.text(`${t.retention} (DAU/MAU): ${retentionRatio}%`, this.margin, y);
|
|
637
|
+
y += 20;
|
|
638
|
+
}
|
|
639
|
+
// Session stats
|
|
640
|
+
if (firebase.averageSessionDuration || firebase.sessionsPerUser) {
|
|
641
|
+
doc.fontSize(11).font(this.getFont());
|
|
642
|
+
const stats = [];
|
|
643
|
+
if (firebase.averageSessionDuration) {
|
|
644
|
+
const minutes = Math.floor(firebase.averageSessionDuration / 60);
|
|
645
|
+
const seconds = Math.floor(firebase.averageSessionDuration % 60);
|
|
646
|
+
stats.push(`${t.avgSessionDuration}: ${minutes}m ${seconds}s`);
|
|
647
|
+
}
|
|
648
|
+
if (firebase.sessionsPerUser) {
|
|
649
|
+
stats.push(`${t.sessionsPerUser}: ${firebase.sessionsPerUser.toFixed(1)}`);
|
|
650
|
+
}
|
|
651
|
+
doc.text(stats.join(" | "), this.margin, y);
|
|
652
|
+
y += 20;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
y += 20;
|
|
656
|
+
// Charts
|
|
657
|
+
const chartWidth = this.contentWidth / 2 - 10;
|
|
658
|
+
const chartHeight = 200;
|
|
659
|
+
// Engagement and Retention charts
|
|
660
|
+
if (options.charts?.engagement) {
|
|
661
|
+
try {
|
|
662
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
663
|
+
doc.text(t.activeUsers, this.margin, y);
|
|
664
|
+
doc.image(options.charts.engagement, this.margin, y + 18, {
|
|
665
|
+
width: chartWidth,
|
|
666
|
+
height: chartHeight,
|
|
667
|
+
fit: [chartWidth, chartHeight],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// Skip if image fails
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (options.charts?.retentionRatio) {
|
|
675
|
+
try {
|
|
676
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
677
|
+
doc.text(t.retention, this.margin + chartWidth + 20, y);
|
|
678
|
+
doc.image(options.charts.retentionRatio, this.margin + chartWidth + 20, y + 18, {
|
|
679
|
+
width: chartWidth,
|
|
680
|
+
height: chartHeight,
|
|
681
|
+
fit: [chartWidth, chartHeight],
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// Skip if image fails
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
y += chartHeight + 50;
|
|
689
|
+
// Demographics charts
|
|
690
|
+
if (options.charts?.demographics) {
|
|
691
|
+
try {
|
|
692
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
693
|
+
doc.text(t.ageDemographics, this.margin, y);
|
|
694
|
+
doc.image(options.charts.demographics, this.margin, y + 18, {
|
|
695
|
+
width: chartWidth,
|
|
696
|
+
height: chartHeight,
|
|
697
|
+
fit: [chartWidth, chartHeight],
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// Skip if image fails
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (options.charts?.countryDistribution) {
|
|
705
|
+
try {
|
|
706
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
707
|
+
doc.text(t.countryDistribution, this.margin + chartWidth + 20, y);
|
|
708
|
+
doc.image(options.charts.countryDistribution, this.margin + chartWidth + 20, y + 18, {
|
|
709
|
+
width: chartWidth,
|
|
710
|
+
height: chartHeight,
|
|
711
|
+
fit: [chartWidth, chartHeight],
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// Skip if image fails
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Add ratings and reviews page.
|
|
721
|
+
*/
|
|
722
|
+
addRatingsReviewsPage(doc, options, startY = this.margin) {
|
|
723
|
+
let y = startY;
|
|
724
|
+
const t = this.translations;
|
|
725
|
+
// Page title
|
|
726
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
727
|
+
doc.text(t.ratingsAndReviews, this.margin, y);
|
|
728
|
+
y += 35;
|
|
729
|
+
// Rating summary
|
|
730
|
+
const googlePlay = options.data.googlePlayConsole;
|
|
731
|
+
const appStore = options.data.appStore;
|
|
732
|
+
const chartWidth = this.contentWidth / 2 - 10;
|
|
733
|
+
const chartHeight = 180;
|
|
734
|
+
if (googlePlay?.averageRating) {
|
|
735
|
+
doc.fillColor(this.colors.googlePlay);
|
|
736
|
+
doc.fontSize(24).font(this.getFont(true));
|
|
737
|
+
doc.text(`${googlePlay.averageRating.toFixed(1)}`, this.margin, y);
|
|
738
|
+
doc.fillColor(this.colors.text);
|
|
739
|
+
doc.fontSize(11).font(this.getFont());
|
|
740
|
+
doc.text(`Google Play (${googlePlay.totalRatings?.toLocaleString() || 0} ratings)`, this.margin + 50, y + 8);
|
|
741
|
+
}
|
|
742
|
+
if (appStore?.averageRating) {
|
|
743
|
+
doc.fillColor(this.colors.appStore);
|
|
744
|
+
doc.fontSize(24).font(this.getFont(true));
|
|
745
|
+
doc.text(`${appStore.averageRating.toFixed(1)}`, this.margin + chartWidth + 20, y);
|
|
746
|
+
doc.fillColor(this.colors.text);
|
|
747
|
+
doc.fontSize(11).font(this.getFont());
|
|
748
|
+
doc.text(`App Store (${appStore.totalRatings?.toLocaleString() || 0} ratings)`, this.margin + chartWidth + 70, y + 8);
|
|
749
|
+
}
|
|
750
|
+
y += 50;
|
|
751
|
+
// Charts
|
|
752
|
+
if (options.charts?.ratingDistribution) {
|
|
753
|
+
try {
|
|
754
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
755
|
+
doc.fillColor(this.colors.text);
|
|
756
|
+
doc.text(t.ratingDistribution, this.margin, y);
|
|
757
|
+
doc.image(options.charts.ratingDistribution, this.margin, y + 18, {
|
|
758
|
+
width: chartWidth,
|
|
759
|
+
height: chartHeight,
|
|
760
|
+
fit: [chartWidth, chartHeight],
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// Skip if image fails
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (options.charts?.sentiment) {
|
|
768
|
+
try {
|
|
769
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
770
|
+
doc.text(t.sentimentAnalysis, this.margin + chartWidth + 20, y);
|
|
771
|
+
doc.image(options.charts.sentiment, this.margin + chartWidth + 20, y + 18, {
|
|
772
|
+
width: chartWidth,
|
|
773
|
+
height: chartHeight,
|
|
774
|
+
fit: [chartWidth, chartHeight],
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Skip if image fails
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
y += chartHeight + 50;
|
|
782
|
+
// Review Analysis
|
|
783
|
+
const reviewAnalysis = options.data.marketingAnalytics?.reviewAnalysis;
|
|
784
|
+
if (reviewAnalysis) {
|
|
785
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
786
|
+
doc.fillColor(this.colors.text);
|
|
787
|
+
doc.text(t.sentimentAnalysis, this.margin, y);
|
|
788
|
+
y += 20;
|
|
789
|
+
// Sentiment breakdown
|
|
790
|
+
if (reviewAnalysis.sentiment) {
|
|
791
|
+
doc.fontSize(11).font(this.getFont());
|
|
792
|
+
doc.fillColor(this.colors.googlePlay);
|
|
793
|
+
doc.text(`${t.positive}: ${reviewAnalysis.sentiment.positive}%`, this.margin, y);
|
|
794
|
+
doc.fillColor(this.colors.neutral);
|
|
795
|
+
doc.text(`${t.neutral}: ${reviewAnalysis.sentiment.neutral}%`, this.margin + 120, y);
|
|
796
|
+
doc.fillColor(this.colors.error);
|
|
797
|
+
doc.text(`${t.negative}: ${reviewAnalysis.sentiment.negative}%`, this.margin + 230, y);
|
|
798
|
+
y += 25;
|
|
799
|
+
}
|
|
800
|
+
// Common themes
|
|
801
|
+
if (reviewAnalysis.commonThemes?.length) {
|
|
802
|
+
doc.fillColor(this.colors.text);
|
|
803
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
804
|
+
doc.text(`${t.commonThemes}:`, this.margin, y);
|
|
805
|
+
y += 18;
|
|
806
|
+
doc.fontSize(10).font(this.getFont());
|
|
807
|
+
for (const theme of reviewAnalysis.commonThemes) {
|
|
808
|
+
doc.text(`- ${theme}`, this.margin + 10, y, { width: this.contentWidth - 20 });
|
|
809
|
+
y = doc.y + 4;
|
|
810
|
+
}
|
|
811
|
+
y += 10;
|
|
812
|
+
}
|
|
813
|
+
// Actionable insights
|
|
814
|
+
if (reviewAnalysis.actionableInsights?.length) {
|
|
815
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
816
|
+
doc.text(`${t.actionableInsights}:`, this.margin, y);
|
|
817
|
+
y += 18;
|
|
818
|
+
doc.fontSize(10).font(this.getFont());
|
|
819
|
+
for (const insight of reviewAnalysis.actionableInsights) {
|
|
820
|
+
doc.fillColor(this.colors.primary);
|
|
821
|
+
doc.text(`-> ${insight}`, this.margin + 10, y, { width: this.contentWidth - 20 });
|
|
822
|
+
y = doc.y + 4;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
doc.fillColor(this.colors.text);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Add improvements page.
|
|
830
|
+
*/
|
|
831
|
+
addImprovementsPage(doc, options, startY = this.margin) {
|
|
832
|
+
let y = startY;
|
|
833
|
+
const suggestions = options.data.marketingAnalytics?.improvementSuggestions || [];
|
|
834
|
+
const t = this.translations;
|
|
835
|
+
// Page title
|
|
836
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
837
|
+
doc.text(t.improvementSuggestions, this.margin, y);
|
|
838
|
+
y += 40;
|
|
839
|
+
for (const suggestion of suggestions) {
|
|
840
|
+
// Check if we need a new page
|
|
841
|
+
if (y > this.maxContentY - 100) {
|
|
842
|
+
this.drawPageFooter(doc);
|
|
843
|
+
doc.addPage();
|
|
844
|
+
this.drawPageBackground(doc);
|
|
845
|
+
y = this.drawPageHeader(doc);
|
|
846
|
+
}
|
|
847
|
+
// Priority badge
|
|
848
|
+
const priorityColor = suggestion.priority === "high"
|
|
849
|
+
? this.colors.error
|
|
850
|
+
: suggestion.priority === "medium"
|
|
851
|
+
? this.colors.warning
|
|
852
|
+
: this.colors.success;
|
|
853
|
+
doc.fillColor(priorityColor);
|
|
854
|
+
doc.rect(this.margin, y, 60, 20).fill();
|
|
855
|
+
doc.fillColor("#ffffff");
|
|
856
|
+
doc.fontSize(10).font(this.getFont(true));
|
|
857
|
+
doc.text(suggestion.priority.toUpperCase(), this.margin + 5, y + 5);
|
|
858
|
+
// Category
|
|
859
|
+
doc.fillColor(this.colors.textSecondary);
|
|
860
|
+
doc.fontSize(10).font(this.getFont());
|
|
861
|
+
doc.text(`[${suggestion.category}]`, this.margin + 70, y + 5);
|
|
862
|
+
y += 25;
|
|
863
|
+
// Title
|
|
864
|
+
doc.fillColor(this.colors.text);
|
|
865
|
+
doc.fontSize(13).font(this.getFont(true));
|
|
866
|
+
doc.text(suggestion.title, this.margin, y);
|
|
867
|
+
y += 20;
|
|
868
|
+
// Description
|
|
869
|
+
doc.fontSize(11).font(this.getFont());
|
|
870
|
+
doc.text(suggestion.description, this.margin + 10, y, {
|
|
871
|
+
width: this.contentWidth - 10,
|
|
872
|
+
});
|
|
873
|
+
y = doc.y + 10;
|
|
874
|
+
// Expected impact
|
|
875
|
+
if (suggestion.expectedImpact) {
|
|
876
|
+
doc.fillColor(this.colors.primary);
|
|
877
|
+
doc.text(`${t.expectedImpact}: ${suggestion.expectedImpact}`, this.margin + 10, y, {
|
|
878
|
+
width: this.contentWidth - 10,
|
|
879
|
+
});
|
|
880
|
+
y = doc.y + 10;
|
|
881
|
+
}
|
|
882
|
+
doc.fillColor(this.colors.text);
|
|
883
|
+
y += 15;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Add competitive positioning page.
|
|
888
|
+
*
|
|
889
|
+
* 競合ポジショニング分析ページを追加。
|
|
890
|
+
*/
|
|
891
|
+
addCompetitivePositioningPage(doc, options, startY = this.margin) {
|
|
892
|
+
let y = startY;
|
|
893
|
+
const positioning = options.data.marketingAnalytics?.competitivePositioning;
|
|
894
|
+
const t = this.translations;
|
|
895
|
+
// Page title
|
|
896
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
897
|
+
doc.text(t.competitivePositioning, this.margin, y);
|
|
898
|
+
y += 35;
|
|
899
|
+
// Market Position
|
|
900
|
+
if (positioning?.marketPosition) {
|
|
901
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
902
|
+
doc.fillColor(this.colors.primary);
|
|
903
|
+
doc.text(t.marketPosition, this.margin, y);
|
|
904
|
+
y += 20;
|
|
905
|
+
doc.fontSize(11).font(this.getFont());
|
|
906
|
+
doc.fillColor(this.colors.text);
|
|
907
|
+
doc.text(positioning.marketPosition, this.margin, y, {
|
|
908
|
+
width: this.contentWidth,
|
|
909
|
+
});
|
|
910
|
+
y = doc.y + 25;
|
|
911
|
+
}
|
|
912
|
+
// Competitor Comparison
|
|
913
|
+
if (positioning?.competitorComparison?.length) {
|
|
914
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
915
|
+
doc.fillColor(this.colors.primary);
|
|
916
|
+
doc.text(t.competitorComparison, this.margin, y);
|
|
917
|
+
y += 25;
|
|
918
|
+
for (const comp of positioning.competitorComparison) {
|
|
919
|
+
// Check if we need a new page
|
|
920
|
+
if (y > this.maxContentY - 150) {
|
|
921
|
+
this.drawPageFooter(doc);
|
|
922
|
+
doc.addPage();
|
|
923
|
+
this.drawPageBackground(doc);
|
|
924
|
+
y = this.drawPageHeader(doc);
|
|
925
|
+
doc.fontSize(16).font(this.getFont(true));
|
|
926
|
+
doc.fillColor(this.colors.text);
|
|
927
|
+
doc.text(`${t.competitivePositioning} ${t.continued}`, this.margin, y);
|
|
928
|
+
y += 30;
|
|
929
|
+
}
|
|
930
|
+
// Competitor name header
|
|
931
|
+
doc.fillColor("#ffffff");
|
|
932
|
+
doc.rect(this.margin, y, this.contentWidth, 22).fill(this.colors.competitorHeader);
|
|
933
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
934
|
+
doc.fillColor("#ffffff");
|
|
935
|
+
doc.text(`vs ${comp.competitor}`, this.margin + 10, y + 5);
|
|
936
|
+
y += 28;
|
|
937
|
+
const halfWidth = this.contentWidth / 2 - 10;
|
|
938
|
+
const itemSpacing = 4;
|
|
939
|
+
const headerHeight = 22;
|
|
940
|
+
const boxPadding = 10;
|
|
941
|
+
// Calculate heights for both sections first
|
|
942
|
+
doc.fontSize(9).font(this.getFont());
|
|
943
|
+
// Calculate Strengths heights
|
|
944
|
+
const strengthHeights = [];
|
|
945
|
+
let totalStrengthsHeight = headerHeight;
|
|
946
|
+
for (const strength of (comp.ourStrengths || []).slice(0, 4)) {
|
|
947
|
+
const h = doc.heightOfString(`✓ ${strength}`, { width: halfWidth - 16 });
|
|
948
|
+
strengthHeights.push(h);
|
|
949
|
+
totalStrengthsHeight += h + itemSpacing;
|
|
950
|
+
}
|
|
951
|
+
totalStrengthsHeight += boxPadding;
|
|
952
|
+
// Calculate Weaknesses heights (with extra right padding)
|
|
953
|
+
const weaknessHeights = [];
|
|
954
|
+
let totalWeaknessesHeight = headerHeight;
|
|
955
|
+
for (const weakness of (comp.ourWeaknesses || []).slice(0, 4)) {
|
|
956
|
+
const h = doc.heightOfString(`△ ${weakness}`, { width: halfWidth - 24 });
|
|
957
|
+
weaknessHeights.push(h);
|
|
958
|
+
totalWeaknessesHeight += h + itemSpacing;
|
|
959
|
+
}
|
|
960
|
+
totalWeaknessesHeight += boxPadding;
|
|
961
|
+
// Sync box heights (use the larger one)
|
|
962
|
+
const boxHeight = Math.max(totalStrengthsHeight, totalWeaknessesHeight, 80);
|
|
963
|
+
// Strengths (left side)
|
|
964
|
+
if (comp.ourStrengths?.length) {
|
|
965
|
+
doc.fillColor(this.colors.highlightBackground);
|
|
966
|
+
doc.rect(this.margin, y, halfWidth, boxHeight).fill();
|
|
967
|
+
doc.fillColor(this.colors.successDark);
|
|
968
|
+
doc.fontSize(10).font(this.getFont(true));
|
|
969
|
+
doc.text(t.ourStrengths, this.margin + 8, y + 5);
|
|
970
|
+
doc.fontSize(9).font(this.getFont());
|
|
971
|
+
let sy = y + headerHeight;
|
|
972
|
+
let i = 0;
|
|
973
|
+
for (const strength of comp.ourStrengths.slice(0, 4)) {
|
|
974
|
+
doc.text(`✓ ${strength}`, this.margin + 8, sy, {
|
|
975
|
+
width: halfWidth - 16,
|
|
976
|
+
});
|
|
977
|
+
sy += strengthHeights[i] + itemSpacing;
|
|
978
|
+
i++;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// Weaknesses (right side)
|
|
982
|
+
if (comp.ourWeaknesses?.length) {
|
|
983
|
+
doc.fillColor(this.colors.concernBackground);
|
|
984
|
+
doc.rect(this.margin + halfWidth + 20, y, halfWidth, boxHeight).fill();
|
|
985
|
+
doc.fillColor(this.colors.errorDark);
|
|
986
|
+
doc.fontSize(10).font(this.getFont(true));
|
|
987
|
+
doc.text(t.ourWeaknesses, this.margin + halfWidth + 28, y + 5);
|
|
988
|
+
doc.fontSize(9).font(this.getFont());
|
|
989
|
+
let wy = y + headerHeight;
|
|
990
|
+
let i = 0;
|
|
991
|
+
for (const weakness of comp.ourWeaknesses.slice(0, 4)) {
|
|
992
|
+
doc.text(`△ ${weakness}`, this.margin + halfWidth + 28, wy, {
|
|
993
|
+
width: halfWidth - 24,
|
|
994
|
+
});
|
|
995
|
+
wy += weaknessHeights[i] + itemSpacing;
|
|
996
|
+
i++;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
y += boxHeight + 10;
|
|
1000
|
+
// Battle Strategy
|
|
1001
|
+
if (comp.battleStrategy) {
|
|
1002
|
+
doc.fillColor(this.colors.summaryBackground);
|
|
1003
|
+
doc.rect(this.margin, y, this.contentWidth, 45).fill();
|
|
1004
|
+
doc.fillColor(this.colors.primary);
|
|
1005
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
1006
|
+
doc.text(`${t.battleStrategy}:`, this.margin + 8, y + 5);
|
|
1007
|
+
doc.fontSize(9).font(this.getFont());
|
|
1008
|
+
doc.text(comp.battleStrategy, this.margin + 8, y + 18, {
|
|
1009
|
+
width: this.contentWidth - 16,
|
|
1010
|
+
height: 24,
|
|
1011
|
+
ellipsis: true,
|
|
1012
|
+
});
|
|
1013
|
+
y += 55;
|
|
1014
|
+
}
|
|
1015
|
+
y += 15;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Check if we need a new page for remaining sections
|
|
1019
|
+
if (y > this.maxContentY - 100) {
|
|
1020
|
+
this.drawPageFooter(doc);
|
|
1021
|
+
doc.addPage();
|
|
1022
|
+
this.drawPageBackground(doc);
|
|
1023
|
+
y = this.drawPageHeader(doc);
|
|
1024
|
+
}
|
|
1025
|
+
// Differentiation Strategy
|
|
1026
|
+
if (positioning?.differentiationStrategy) {
|
|
1027
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
1028
|
+
doc.fillColor(this.colors.primary);
|
|
1029
|
+
doc.text(t.differentiationStrategy, this.margin, y);
|
|
1030
|
+
y += 20;
|
|
1031
|
+
doc.fontSize(10).font(this.getFont());
|
|
1032
|
+
doc.fillColor(this.colors.text);
|
|
1033
|
+
doc.text(positioning.differentiationStrategy, this.margin, y, {
|
|
1034
|
+
width: this.contentWidth,
|
|
1035
|
+
});
|
|
1036
|
+
y = doc.y + 25;
|
|
1037
|
+
}
|
|
1038
|
+
// Quick Wins
|
|
1039
|
+
if (positioning?.quickWins?.length) {
|
|
1040
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
1041
|
+
doc.fillColor(this.colors.primary);
|
|
1042
|
+
doc.text(t.quickWins, this.margin, y);
|
|
1043
|
+
y += 20;
|
|
1044
|
+
doc.fontSize(10).font(this.getFont());
|
|
1045
|
+
for (const quickWin of positioning.quickWins) {
|
|
1046
|
+
doc.fillColor(this.colors.success);
|
|
1047
|
+
doc.text(`→ ${quickWin}`, this.margin + 10, y, { width: this.contentWidth - 20 });
|
|
1048
|
+
y = doc.y + 5;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
doc.fillColor(this.colors.text);
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Add market opportunity priority page.
|
|
1055
|
+
*
|
|
1056
|
+
* 市場機会優先度分析ページを追加。
|
|
1057
|
+
*/
|
|
1058
|
+
addMarketOpportunityPriorityPage(doc, options, startY = this.margin) {
|
|
1059
|
+
let y = startY;
|
|
1060
|
+
const priority = options.data.marketingAnalytics?.marketOpportunityPriority;
|
|
1061
|
+
const t = this.translations;
|
|
1062
|
+
// Page title
|
|
1063
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
1064
|
+
doc.text(t.marketOpportunityPriority, this.margin, y);
|
|
1065
|
+
y += 35;
|
|
1066
|
+
// Prioritized Opportunities
|
|
1067
|
+
if (priority?.prioritizedOpportunities?.length) {
|
|
1068
|
+
const fitScoreColors = {
|
|
1069
|
+
excellent: this.colors.success,
|
|
1070
|
+
good: this.colors.primary,
|
|
1071
|
+
moderate: this.colors.warning,
|
|
1072
|
+
poor: this.colors.error,
|
|
1073
|
+
};
|
|
1074
|
+
const effortLabels = {
|
|
1075
|
+
low: t.low,
|
|
1076
|
+
medium: t.medium,
|
|
1077
|
+
high: t.high,
|
|
1078
|
+
};
|
|
1079
|
+
for (const opp of priority.prioritizedOpportunities) {
|
|
1080
|
+
// Check if we need a new page
|
|
1081
|
+
if (y > this.maxContentY - 130) {
|
|
1082
|
+
this.drawPageFooter(doc);
|
|
1083
|
+
doc.addPage();
|
|
1084
|
+
this.drawPageBackground(doc);
|
|
1085
|
+
y = this.drawPageHeader(doc);
|
|
1086
|
+
doc.fontSize(16).font(this.getFont(true));
|
|
1087
|
+
doc.fillColor(this.colors.text);
|
|
1088
|
+
doc.text(`${t.marketOpportunityPriority} ${t.continued}`, this.margin, y);
|
|
1089
|
+
y += 30;
|
|
1090
|
+
}
|
|
1091
|
+
const fitColor = fitScoreColors[opp.fitScore] || this.colors.textSecondary;
|
|
1092
|
+
// Opportunity header with fit score badge
|
|
1093
|
+
doc.fillColor(fitColor);
|
|
1094
|
+
doc.rect(this.margin, y, 70, 20).fill();
|
|
1095
|
+
doc.fillColor("#ffffff");
|
|
1096
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
1097
|
+
doc.text(opp.fitScore.toUpperCase(), this.margin + 5, y + 5);
|
|
1098
|
+
// Effort badge
|
|
1099
|
+
doc.fillColor(this.colors.textSecondary);
|
|
1100
|
+
doc.rect(this.margin + 75, y, 70, 20).fill();
|
|
1101
|
+
doc.fillColor("#ffffff");
|
|
1102
|
+
doc.fontSize(9).font(this.getFont());
|
|
1103
|
+
doc.text(`${t.effort}:${effortLabels[opp.estimatedEffort] || opp.estimatedEffort}`, this.margin + 80, y + 5, {
|
|
1104
|
+
width: 60,
|
|
1105
|
+
height: 12,
|
|
1106
|
+
ellipsis: true,
|
|
1107
|
+
});
|
|
1108
|
+
y += 25;
|
|
1109
|
+
// Opportunity title
|
|
1110
|
+
doc.fillColor(this.colors.text);
|
|
1111
|
+
doc.fontSize(13).font(this.getFont(true));
|
|
1112
|
+
doc.text(opp.opportunity, this.margin, y);
|
|
1113
|
+
y += 20;
|
|
1114
|
+
// Fit reason
|
|
1115
|
+
if (opp.fitReason) {
|
|
1116
|
+
doc.fontSize(10).font(this.getFont());
|
|
1117
|
+
doc.fillColor(this.colors.textSecondary);
|
|
1118
|
+
doc.text(`${t.reason}: ${opp.fitReason}`, this.margin + 10, y, {
|
|
1119
|
+
width: this.contentWidth - 10,
|
|
1120
|
+
});
|
|
1121
|
+
y = doc.y + 10;
|
|
1122
|
+
}
|
|
1123
|
+
// Required changes
|
|
1124
|
+
if (opp.requiredChanges?.length) {
|
|
1125
|
+
doc.fontSize(10).font(this.getFont(true));
|
|
1126
|
+
doc.fillColor(this.colors.text);
|
|
1127
|
+
doc.text(`${t.requiredChanges}:`, this.margin + 10, y);
|
|
1128
|
+
y += 15;
|
|
1129
|
+
doc.fontSize(9).font(this.getFont());
|
|
1130
|
+
for (const change of opp.requiredChanges.slice(0, 3)) {
|
|
1131
|
+
doc.fillColor(this.colors.primary);
|
|
1132
|
+
doc.text(`• ${change}`, this.margin + 20, y, { width: this.contentWidth - 30 });
|
|
1133
|
+
y = doc.y + 3;
|
|
1134
|
+
}
|
|
1135
|
+
if (opp.requiredChanges.length > 3) {
|
|
1136
|
+
doc.fillColor(this.colors.textSecondary);
|
|
1137
|
+
doc.text(`+ ${opp.requiredChanges.length - 3} more...`, this.margin + 20, y);
|
|
1138
|
+
y = doc.y + 3;
|
|
1139
|
+
}
|
|
1140
|
+
y += 5;
|
|
1141
|
+
}
|
|
1142
|
+
// Recommended action
|
|
1143
|
+
if (opp.recommendedAction) {
|
|
1144
|
+
doc.fillColor(this.colors.highlightBackground);
|
|
1145
|
+
doc.rect(this.margin + 10, y, this.contentWidth - 20, 40).fill();
|
|
1146
|
+
doc.fillColor(this.colors.successDark);
|
|
1147
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
1148
|
+
doc.text(`${t.recommendedAction}:`, this.margin + 15, y + 5);
|
|
1149
|
+
doc.fontSize(9).font(this.getFont());
|
|
1150
|
+
doc.text(opp.recommendedAction, this.margin + 15, y + 17, {
|
|
1151
|
+
width: this.contentWidth - 30,
|
|
1152
|
+
height: 20,
|
|
1153
|
+
ellipsis: true,
|
|
1154
|
+
});
|
|
1155
|
+
y += 50;
|
|
1156
|
+
}
|
|
1157
|
+
y += 15;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Check if we need a new page for strategic recommendation
|
|
1161
|
+
if (y > this.maxContentY - 50) {
|
|
1162
|
+
this.drawPageFooter(doc);
|
|
1163
|
+
doc.addPage();
|
|
1164
|
+
this.drawPageBackground(doc);
|
|
1165
|
+
y = this.drawPageHeader(doc);
|
|
1166
|
+
}
|
|
1167
|
+
// Strategic Recommendation
|
|
1168
|
+
if (priority?.strategicRecommendation) {
|
|
1169
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
1170
|
+
doc.fillColor(this.colors.primary);
|
|
1171
|
+
doc.text(t.strategicRecommendations, this.margin, y);
|
|
1172
|
+
y += 20;
|
|
1173
|
+
doc.fillColor(this.colors.summaryBackground);
|
|
1174
|
+
doc.rect(this.margin, y, this.contentWidth, 80).fill();
|
|
1175
|
+
doc.fontSize(10).font(this.getFont());
|
|
1176
|
+
doc.fillColor(this.colors.primary);
|
|
1177
|
+
doc.text(priority.strategicRecommendation, this.margin + 10, y + 10, {
|
|
1178
|
+
width: this.contentWidth - 20,
|
|
1179
|
+
height: 65,
|
|
1180
|
+
ellipsis: true,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
doc.fillColor(this.colors.text);
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Add trend analysis page.
|
|
1187
|
+
*/
|
|
1188
|
+
addTrendAnalysisPage(doc, options, startY = this.margin) {
|
|
1189
|
+
let y = startY;
|
|
1190
|
+
const trendAnalysis = options.data.marketingAnalytics?.trendAnalysis;
|
|
1191
|
+
const t = this.translations;
|
|
1192
|
+
// Page title
|
|
1193
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
1194
|
+
doc.text(t.trendAnalysisAndPredictions, this.margin, y);
|
|
1195
|
+
y += 35;
|
|
1196
|
+
// User Growth Trend
|
|
1197
|
+
if (trendAnalysis?.userGrowthTrend) {
|
|
1198
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
1199
|
+
doc.text(t.userGrowth, this.margin, y);
|
|
1200
|
+
y += 18;
|
|
1201
|
+
doc.fontSize(10).font(this.getFont());
|
|
1202
|
+
doc.text(trendAnalysis.userGrowthTrend, this.margin, y, {
|
|
1203
|
+
width: this.contentWidth,
|
|
1204
|
+
});
|
|
1205
|
+
y = doc.y + 20;
|
|
1206
|
+
}
|
|
1207
|
+
// Engagement Trend
|
|
1208
|
+
if (trendAnalysis?.engagementTrend) {
|
|
1209
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
1210
|
+
doc.text(t.engagement, this.margin, y);
|
|
1211
|
+
y += 18;
|
|
1212
|
+
doc.fontSize(10).font(this.getFont());
|
|
1213
|
+
doc.text(trendAnalysis.engagementTrend, this.margin, y, {
|
|
1214
|
+
width: this.contentWidth,
|
|
1215
|
+
});
|
|
1216
|
+
y = doc.y + 20;
|
|
1217
|
+
}
|
|
1218
|
+
// Rating Trend
|
|
1219
|
+
if (trendAnalysis?.ratingTrend) {
|
|
1220
|
+
doc.fontSize(12).font(this.getFont(true));
|
|
1221
|
+
doc.text(t.ratings, this.margin, y);
|
|
1222
|
+
y += 18;
|
|
1223
|
+
doc.fontSize(10).font(this.getFont());
|
|
1224
|
+
doc.text(trendAnalysis.ratingTrend, this.margin, y, {
|
|
1225
|
+
width: this.contentWidth,
|
|
1226
|
+
});
|
|
1227
|
+
y = doc.y + 30;
|
|
1228
|
+
}
|
|
1229
|
+
// Predictions
|
|
1230
|
+
if (trendAnalysis?.predictions?.length) {
|
|
1231
|
+
doc.fontSize(14).font(this.getFont(true));
|
|
1232
|
+
doc.text(t.predictions, this.margin, y);
|
|
1233
|
+
y += 22;
|
|
1234
|
+
doc.fontSize(10).font(this.getFont());
|
|
1235
|
+
for (const prediction of trendAnalysis.predictions) {
|
|
1236
|
+
doc.fillColor(this.colors.primary);
|
|
1237
|
+
doc.text(`-> ${prediction}`, this.margin + 10, y, { width: this.contentWidth - 20 });
|
|
1238
|
+
y = doc.y + 6;
|
|
1239
|
+
}
|
|
1240
|
+
doc.fillColor(this.colors.text);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Add GitHub-based code improvements page.
|
|
1245
|
+
*
|
|
1246
|
+
* GitHubベースのコード改善提案ページを追加。
|
|
1247
|
+
*/
|
|
1248
|
+
addGitHubImprovementsPage(doc, options, startY = this.margin) {
|
|
1249
|
+
let y = startY;
|
|
1250
|
+
const githubImprovements = options.data.githubImprovements;
|
|
1251
|
+
const improvements = githubImprovements?.improvements || [];
|
|
1252
|
+
const t = this.translations;
|
|
1253
|
+
// Page title
|
|
1254
|
+
doc.fontSize(20).font(this.getFont(true));
|
|
1255
|
+
doc.text(t.codebaseImprovements, this.margin, y);
|
|
1256
|
+
y += 25;
|
|
1257
|
+
// Repository info
|
|
1258
|
+
doc.fontSize(10).font(this.getFont());
|
|
1259
|
+
doc.fillColor(this.colors.textSecondary);
|
|
1260
|
+
const repo = githubImprovements?.repository || "";
|
|
1261
|
+
const framework = githubImprovements?.framework || "";
|
|
1262
|
+
doc.text(`${t.repository}: ${repo} | ${t.framework}: ${framework}`, this.margin, y);
|
|
1263
|
+
y += 20;
|
|
1264
|
+
// Summary
|
|
1265
|
+
if (githubImprovements?.improvementSummary) {
|
|
1266
|
+
doc.fillColor(this.colors.text);
|
|
1267
|
+
doc.fontSize(11).font(this.getFont());
|
|
1268
|
+
doc.text(githubImprovements.improvementSummary, this.margin, y, {
|
|
1269
|
+
width: this.contentWidth,
|
|
1270
|
+
});
|
|
1271
|
+
y = doc.y + 25;
|
|
1272
|
+
}
|
|
1273
|
+
// Each improvement
|
|
1274
|
+
for (const improvement of improvements) {
|
|
1275
|
+
// Check if we need a new page
|
|
1276
|
+
if (y > this.maxContentY - 150) {
|
|
1277
|
+
this.drawPageFooter(doc);
|
|
1278
|
+
doc.addPage();
|
|
1279
|
+
this.drawPageBackground(doc);
|
|
1280
|
+
y = this.drawPageHeader(doc);
|
|
1281
|
+
doc.fontSize(16).font(this.getFont(true));
|
|
1282
|
+
doc.fillColor(this.colors.text);
|
|
1283
|
+
doc.text(`${t.codebaseImprovements} ${t.continued}`, this.margin, y);
|
|
1284
|
+
y += 30;
|
|
1285
|
+
}
|
|
1286
|
+
// Priority badge
|
|
1287
|
+
const priorityColor = improvement.priority === "high"
|
|
1288
|
+
? this.colors.error
|
|
1289
|
+
: improvement.priority === "medium"
|
|
1290
|
+
? this.colors.warning
|
|
1291
|
+
: this.colors.success;
|
|
1292
|
+
doc.fillColor(priorityColor);
|
|
1293
|
+
doc.rect(this.margin, y, 60, 18).fill();
|
|
1294
|
+
doc.fillColor("#ffffff");
|
|
1295
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
1296
|
+
doc.text(improvement.priority?.toUpperCase() || "MEDIUM", this.margin + 5, y + 4);
|
|
1297
|
+
// Category badge
|
|
1298
|
+
doc.fillColor(this.colors.primary);
|
|
1299
|
+
doc.rect(this.margin + 65, y, 80, 18).fill();
|
|
1300
|
+
doc.fillColor("#ffffff");
|
|
1301
|
+
doc.fontSize(9).font(this.getFont());
|
|
1302
|
+
doc.text(improvement.category || "", this.margin + 70, y + 4);
|
|
1303
|
+
y += 25;
|
|
1304
|
+
// Title
|
|
1305
|
+
doc.fillColor(this.colors.text);
|
|
1306
|
+
doc.fontSize(13).font(this.getFont(true));
|
|
1307
|
+
doc.text(improvement.title || "", this.margin, y);
|
|
1308
|
+
y += 20;
|
|
1309
|
+
// Description
|
|
1310
|
+
doc.fontSize(10).font(this.getFont());
|
|
1311
|
+
doc.text(improvement.description || "", this.margin + 10, y, {
|
|
1312
|
+
width: this.contentWidth - 10,
|
|
1313
|
+
});
|
|
1314
|
+
y = doc.y + 10;
|
|
1315
|
+
// Related Feature
|
|
1316
|
+
if (improvement.relatedFeature) {
|
|
1317
|
+
doc.fillColor("#6a1b9a");
|
|
1318
|
+
doc.fontSize(9).font(this.getFont());
|
|
1319
|
+
doc.text(`${t.relatedFeature}: ${improvement.relatedFeature}`, this.margin + 10, y);
|
|
1320
|
+
y = doc.y + 8;
|
|
1321
|
+
}
|
|
1322
|
+
// Code References
|
|
1323
|
+
const codeRefs = improvement.codeReferences || [];
|
|
1324
|
+
if (codeRefs.length > 0) {
|
|
1325
|
+
doc.fillColor(this.colors.text);
|
|
1326
|
+
doc.fontSize(10).font(this.getFont(true));
|
|
1327
|
+
doc.text(`${t.fileModifications}:`, this.margin + 10, y);
|
|
1328
|
+
y += 15;
|
|
1329
|
+
for (const ref of codeRefs) {
|
|
1330
|
+
// Check page break
|
|
1331
|
+
if (y > this.maxContentY - 50) {
|
|
1332
|
+
this.drawPageFooter(doc);
|
|
1333
|
+
doc.addPage();
|
|
1334
|
+
this.drawPageBackground(doc);
|
|
1335
|
+
y = this.drawPageHeader(doc);
|
|
1336
|
+
doc.fontSize(16).font(this.getFont(true));
|
|
1337
|
+
doc.fillColor(this.colors.text);
|
|
1338
|
+
doc.text(`${t.codebaseImprovements} ${t.continued}`, this.margin, y);
|
|
1339
|
+
y += 30;
|
|
1340
|
+
}
|
|
1341
|
+
// Modification type icon
|
|
1342
|
+
const modIcon = {
|
|
1343
|
+
add: "+",
|
|
1344
|
+
modify: "~",
|
|
1345
|
+
refactor: "R",
|
|
1346
|
+
optimize: "O",
|
|
1347
|
+
};
|
|
1348
|
+
const modColor = {
|
|
1349
|
+
add: this.colors.success,
|
|
1350
|
+
modify: this.colors.warning,
|
|
1351
|
+
refactor: this.colors.primary,
|
|
1352
|
+
optimize: "#6a1b9a",
|
|
1353
|
+
};
|
|
1354
|
+
const icon = modIcon[ref.modificationType] || "?";
|
|
1355
|
+
const color = modColor[ref.modificationType] || this.colors.textSecondary;
|
|
1356
|
+
// Modification type badge
|
|
1357
|
+
doc.fillColor(color);
|
|
1358
|
+
doc.roundedRect(this.margin + 15, y, 20, 16, 2).fill();
|
|
1359
|
+
doc.fillColor("#ffffff");
|
|
1360
|
+
doc.fontSize(9).font(this.getFont(true));
|
|
1361
|
+
doc.text(icon, this.margin + 21, y + 3);
|
|
1362
|
+
// File path
|
|
1363
|
+
doc.fillColor(this.colors.primary);
|
|
1364
|
+
doc.fontSize(9).font(this.getFont());
|
|
1365
|
+
doc.text(ref.filePath || "", this.margin + 40, y + 3, {
|
|
1366
|
+
width: this.contentWidth - 50,
|
|
1367
|
+
});
|
|
1368
|
+
y += 18;
|
|
1369
|
+
// Current functionality
|
|
1370
|
+
doc.fillColor(this.colors.textSecondary);
|
|
1371
|
+
doc.fontSize(8).font(this.getFont());
|
|
1372
|
+
doc.text(`${t.current}: ${ref.currentFunctionality || ""}`, this.margin + 40, y, {
|
|
1373
|
+
width: this.contentWidth - 50,
|
|
1374
|
+
});
|
|
1375
|
+
y = doc.y + 3;
|
|
1376
|
+
// Proposed change
|
|
1377
|
+
doc.fillColor(this.colors.success);
|
|
1378
|
+
doc.text(`${t.proposed}: ${ref.proposedChange || ""}`, this.margin + 40, y, {
|
|
1379
|
+
width: this.contentWidth - 50,
|
|
1380
|
+
});
|
|
1381
|
+
y = doc.y + 8;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
// Expected Impact
|
|
1385
|
+
if (improvement.expectedImpact) {
|
|
1386
|
+
doc.fillColor(this.colors.primary);
|
|
1387
|
+
doc.fontSize(9).font(this.getFont());
|
|
1388
|
+
doc.text(`${t.expectedImpact}: ${improvement.expectedImpact}`, this.margin + 10, y);
|
|
1389
|
+
y = doc.y + 5;
|
|
1390
|
+
}
|
|
1391
|
+
doc.fillColor(this.colors.text);
|
|
1392
|
+
y += 20;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
exports.PDFService = PDFService;
|
|
1397
|
+
//# sourceMappingURL=pdf_service.js.map
|