-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Open
Description
When generating PDFs, the text inside certain table rows is breaking in the middle of words, causing readability issues (e.g., "conve\nrsation" instead of "conversation").

My method is
async generatePDF(selectedReport?: any) {
const selectedReports = this.sitesReportArray.filter((report) => report.checked);
if (selectedReport) selectedReports.length = 1;
const doc = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a3',
});
const pageWidth = doc.internal.pageSize.getWidth();
const contentWidth = pageWidth - 20;
const leftMargin = 10;
const halfWidth = contentWidth / 2;
const rightMargin = leftMargin + halfWidth + 93;
doc.setFontSize(18).setFont('helvetica', 'bold').text('Site Report', 10, 15);
const img = new Image();
img.src = 'assets/Flash-security.png';
await new Promise<void>((resolve) => {
img.onload = () => {
doc.addImage(img, 'PNG', pageWidth - 100, 0, 90, 20);
resolve();
};
});
console.log("This is Report:", this.reportArray);
if (this.reportArray) {
const reportLogs = this.reportArray;
let y = 25;
const padding = 10;
const blockStartX = leftMargin;
const blockStartY = y;
const blockWidth = pageWidth - 2 * leftMargin;
let tempLeftY = y + padding;
let tempRightY = y + padding;
doc.setFontSize(14).setFont('helvetica', 'normal');
const siteNameLines = doc.splitTextToSize(`Site Name: ${reportLogs[0]?.siteName || 'N/A'}`, 130);
tempLeftY += siteNameLines.length * 6;
const siteAddressLines = doc.splitTextToSize(`Site Address: ${reportLogs[0]?.siteAddress || 'N/A'}`, 130);
tempLeftY += siteAddressLines.length * 6;
tempLeftY += 6; // Assigned Manager
tempRightY += 6; // Date
tempRightY += 6; // Time
tempRightY += 6; // Staff
const blockHeight = Math.max(tempLeftY, tempRightY) - y + padding;
doc.setFillColor(242, 244, 246);
doc.roundedRect(blockStartX, blockStartY, blockWidth, blockHeight, 1, 1, 'F');
doc.setTextColor(52, 52, 52);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('Site Details', leftMargin + padding, y + padding);
doc.text('Shift Details', rightMargin, y + padding);
let leftY = y + padding + 8;
let rightY = y + padding + 8;
doc.setFontSize(12);
// --- Site Name ---
const siteName = this.siteDetail['siteName'] || 'N/A';
const siteNameLabel = 'Site Name: ';
const siteNameLabelWidth = doc.getTextWidth(siteNameLabel);
doc.setFont('helvetica', 'bold');
doc.text(siteNameLabel, leftMargin + padding, leftY);
doc.setFont('helvetica', 'normal');
doc.text(siteName, leftMargin + padding + siteNameLabelWidth, leftY);
leftY += 6;
// --- Site Address ---
const siteAddress = this.siteDetail['locationAddress'] || 'N/A';
const siteAddressLabel = 'Site Address: ';
const siteAddressLabelWidth = doc.getTextWidth(siteAddressLabel);
const spacingBetweenLabelAndValue = 5 / 3.7795; // 5px ≈ 1.3229 mm (1 px ≈ 0.264583 mm)
doc.setFont('helvetica', 'bold');
doc.text(siteAddressLabel, leftMargin + padding, leftY);
doc.setFont('helvetica', 'normal');
const wrappedAddress = doc.splitTextToSize(siteAddress, 130);
doc.text(
wrappedAddress,
leftMargin + padding + siteAddressLabelWidth + spacingBetweenLabelAndValue,
leftY
);
leftY += wrappedAddress.length * 6;
// --- Assigned Manager ---
const managerLabel = 'Staff: ';
const manager = `${reportLogs[0]?.firstName} ${reportLogs[0]?.lastName}` || 'N/A';;
const managerLabelWidth = doc.getTextWidth(managerLabel);
doc.setFont('helvetica', 'bold');
doc.text(managerLabel, leftMargin + padding, leftY);
doc.setFont('helvetica', 'normal');
doc.text(manager, leftMargin + padding + managerLabelWidth, leftY);
// --- Date ---
const shiftDate = reportLogs[0]?.startDate;
const formattedDate = shiftDate
? new Date(shiftDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
timeZone: 'UTC'
})
: 'N/A';
const dateLabel = 'Date: ';
const dateLabelWidth = doc.getTextWidth(dateLabel);
doc.setFont('helvetica', 'bold');
doc.text(dateLabel, rightMargin, rightY);
doc.setFont('helvetica', 'normal');
doc.text(formattedDate, rightMargin + dateLabelWidth, rightY);
rightY += 6;
// --- Time ---
const timeLabel = 'Time: ';
const timeValue = `${reportLogs[0]?.shiftStartTime || ''} - ${reportLogs[0]?.shiftEndTime || ''}`;
const timeLabelWidth = doc.getTextWidth(timeLabel);
doc.setFont('helvetica', 'bold');
doc.text(timeLabel, rightMargin, rightY);
doc.setFont('helvetica', 'normal');
doc.text(timeValue, rightMargin + timeLabelWidth, rightY);
rightY += 6;
doc.setTextColor
(0, 0, 0); // reset color
y = blockStartY + blockHeight + 10;
const tableHeaders = [['Time', 'Date', 'Description']];
const tableData: any[] = [];
const rowHeights: number[] = [];
const inlineDescriptions: any[][] = [];
const imageMap: any[] = [];
console.log("This is selectedState Value:", this.reportArray);
tableData.push(['', '', '']);
console.log("FDsdsa")
rowHeights.push(2);
inlineDescriptions.push([]);
const uniqueLogMessages = new Set<string>();
console.log("This is selectedState Value:", this.reportArray);
for (let log of this.reportArray) {
if (!log?.createdDate) {
console.warn('Skipping log due to missing createdDate:', log);
continue;
}
const key = `${log.createdDate}-${log.firstName}-${log.lastName}-${log.logMessage}`;
if (uniqueLogMessages.has(key)) {
continue;
}
uniqueLogMessages.add(key);
const time = this.formatCreatedDate(log?.createdDate, this.selectedTimeFormat);
const formattedDate = new Date(log.createdDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
const dateTime = `${formattedDate}\n${time}`;
const fullName = `${log.firstName} ${log.lastName}`;
let logDescription = `Staff: ${fullName}\n`;
// Prepare clean logMessage
let rawLogMessage = log.logMessage || '';
let reportValue = log?.report[0]?.reportId || '';
let displayLogMessage = '';
console.log("Value:", reportValue);
if (rawLogMessage.startsWith('Disclaimer')) {
displayLogMessage = 'Disclaimer';
doc.setTextColor(0, 128, 0);
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else if (rawLogMessage.startsWith('Job-Duty')) {
displayLogMessage = 'Job Duty';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
}
else if (reportValue == '824') {
displayLogMessage = 'Incident';
logDescription = `${displayLogMessage} - Complete at ${time}\n` + logDescription;
} else if (rawLogMessage.startsWith('Scheduling accepted')) {
displayLogMessage = 'Accepted';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else if (rawLogMessage.startsWith('Guard is starting')) {
displayLogMessage = 'Duty Started';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else if (rawLogMessage.startsWith('Security Guard ended')) {
displayLogMessage = 'Duty Ended';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else if (rawLogMessage.includes('battery remaining')) {
displayLogMessage = 'Battery Log';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else if (rawLogMessage.includes('Custom Report')) {
displayLogMessage = 'Custom Report';
logDescription = `${displayLogMessage} at ${time}\n` + logDescription;
} else {
displayLogMessage = rawLogMessage;
}
// Append meaningful message to description
if (rawLogMessage.includes('late')) {
const lateMinutes = rawLogMessage.match(/\d+/)?.[0] || 'N/A';
logDescription += `Guard started ${lateMinutes} minutes late\n`;
}
else if (rawLogMessage.includes('Job-Duty') && !rawLogMessage.includes(fullName)) {
logDescription;
}
else if (rawLogMessage.includes('Disclaimer') && !rawLogMessage.includes(fullName)) {
logDescription;
}
else if (rawLogMessage.includes('Custom Report') && !rawLogMessage.includes(fullName)) {
logDescription;
}
else if (rawLogMessage.includes('battery remaining') && !rawLogMessage.includes(fullName)) {
const lateMinutess = rawLogMessage.match(/\d+/)?.[0] || 'N/A';
logDescription += `The guard has ${lateMinutess}% battery remaining\n`;
}
else if (rawLogMessage && !rawLogMessage.includes(fullName)) {
logDescription += `${rawLogMessage}\n`;
}
// Process report parts
const parts: any[] = [];
if (log.report?.length) {
if (tableData.length !== 0) {
parts.push({ type: 'text', content: '\n' });
}
for (const report of log.report) {
if (!this.isURL(report.reportValue)) {
const contentLines = [
`{highlight}${report?.reportFieldName}:{/highlight} ${report.reportValue}`
].filter(line => !!line && line !== 'null' && line !== 'undefined');
if (contentLines.length > 0) {
const rawContent = contentLines.join('\n');
const wrappedText = doc.splitTextToSize(rawContent, contentWidth - 150);
parts.push({
type: 'text',
content: ''
});
parts.push({
type: 'text',
content: wrappedText.join('\n')
});
}
} else {
const urls = report.reportValue.split(',').map((url) => url.trim());
for (const url of urls) {
imageMap.push({ url, logIndex: tableData.length });
parts.push({ type: 'image', url });
}
}
}
}
// Estimate cell height
inlineDescriptions.push(parts);
let estimatedHeight = 0;
let imageCount = 0;
for (const part of parts) {
if (part.type === 'text') {
const lines = part.content.split('\n');
estimatedHeight += lines.length * 5;
} else if (part.type === 'image') {
imageCount += 1;
}
}
if (imageCount) {
estimatedHeight += Math.ceil(imageCount / 4) * 30;
}
rowHeights.push(estimatedHeight);
console.log("This is messgae:", logDescription)
tableData.push([dateTime, displayLogMessage, logDescription]);
}
for (const image of imageMap) {
try {
const base64 = await this.convertImageToBase64(image.url);
image.base64 = base64;
} catch (err) {
console.warn(`Failed to load image: ${image.url}`);
}
}
autoTable(doc, {
startY: y,
head: tableHeaders,
body: tableData,
useCss: true,
rowPageBreak: 'auto',
margin: { top: 10, left: 10, right: 10, bottom: 40 },
styles: {
fontSize: 13,
cellPadding: 3,
valign: 'top',
cellWidth: 'wrap',
},
// pageBreak: 'always',
headStyles: {
fillColor: [247, 134, 30],
textColor: 255,
cellPadding: 4,
lineWidth: 0.9,
fontStyle: 'bold',
},
columnStyles: {
0: { cellWidth: 50 },
1: { cellWidth: 60 },
2: { cellWidth: contentWidth - 110 },
},
showHead: 'firstPage',
willDrawCell: (data) => {
const height = rowHeights[data.row.index];
if (data.section === 'body' && data.column.index === 1 && height) {
data.row.height = height;
}
},
didDrawPage: (data) => {
const bottomPadding = 20; // mm
const pageHeight = doc.internal.pageSize.getHeight();
const pageWidth = doc.internal.pageSize.getWidth();
doc.setFillColor(255, 255, 255); // or any background
doc.rect(0, pageHeight - bottomPadding, pageWidth, bottomPadding, 'F');
},
didParseCell: (data) => {
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Disclaimer') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Job Duty') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Accepted') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Duty Started') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Duty Ended') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Battery Log') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Custom Report') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Custom Report') {
data.cell.text = []; // Prevent default rendering
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Incident') {
data.cell.text = []; // Prevent default rendering
}
},
didDrawCell: (data) => {
const logIndex = data.row.index;
console.log("This is Data :", data);
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Disclaimer') {
doc.setTextColor('#009D10');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Disclaimer', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Job Duty') {
doc.setTextColor('#F7861E');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Job Duty', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Incident') {
doc.setTextColor('#FF3030');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Incident', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Accepted') {
doc.setTextColor('#009D10');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Accepted', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Duty Started') {
doc.setTextColor('#0a70ea');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Duty Started', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Duty Ended') {
doc.setTextColor('#0a70ea');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Duty Ended', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Battery Log') {
doc.setTextColor('#FF3030');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Battery Log', textX, textY);
doc.setTextColor('#000000');
}
if (data.column.index === 1 && String(data.cell.raw).trim() === 'Custom Report') {
doc.setTextColor('#4F4F4F');
const textX = data.cell.x + 2;
const fontSize = doc.getFontSize();
const ascent = fontSize * 0.6;
const textY = data.cell.y + ascent;
doc.text('Custom Report', textX, textY);
doc.setTextColor('#000000');
}
// ✅ Custom render for rich descriptions in column 2
if (data.column.index === 2 && typeof logIndex === 'number') {
const rowRaw = data.row.raw;
// Only handle body rows and column index 1 (log type)
console.log("Value :", rowRaw);
if (data.section === 'body' && data.column.index === 2) {
console.log("Value 1:", rowRaw);
}
data.cell.text = []; // Prevent default rendering
const descriptionParts = inlineDescriptions[logIndex] || [];
let currentY = data.cell.y;
const textX = data.cell.x + 3;
for (const part of descriptionParts) {
if (part.type === 'text') {
const lines = doc.splitTextToSize(part.content, data.cell.width - 6);
for (const line of lines) {
let lineX = textX;
const tokens = line.split(/({highlight}|{\/highlight})/);
let isHighlighted = false;
for (const token of tokens) {
if (token === '{highlight}') {
isHighlighted = true;
continue;
} else if (token === '{/highlight}') {
isHighlighted = false;
continue;
}
if (token.trim() !== '') {
doc.setTextColor('#000000');
doc.setFont('helvetica', isHighlighted ? 'bold' : 'normal');
doc.text(token, lineX, currentY);
lineX += doc.getTextWidth(token);
}
}
currentY += 5;
}
currentY += 1;
}
}
let imageX = textX;
let imageY = currentY + 2;
let imagesInRow = 0;
const seenImages = new Set<string>();
for (const part of descriptionParts) {
if (part.type === 'image') {
const imageKey = `${logIndex}-${part.url}`;
if (seenImages.has(imageKey)) continue;
seenImages.add(imageKey);
const imgData = imageMap.find(
(i) => i.url === part.url && i.logIndex === logIndex
);
if (imgData?.base64) {
doc.addImage(imgData.base64, 'JPEG', imageX, imageY, 18, 18);
doc.link(imageX, imageY, 18, 18, { url: imgData.url });
imageX += 25;
imagesInRow++;
if (imagesInRow >= 4) {
imageX = textX;
imageY += 30;
imagesInRow = 0;
}
}
}
}
}
}
});
}
let date = this.reportArray[0]?.createdDate?.slice(0, 10);
let name = `ClientReport_${date}_${this.reportArray[0]?.siteName}`
doc.save(`${name}.pdf`);
}
Metadata
Metadata
Assignees
Labels
No labels