Skip to content

Text Breaking Mid-Word in PDF Table Rows #3870

@mimran25

Description

@mimran25

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").

Image

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions