@otntn1145

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

kintone上で表示されてるHTMLをPDF出力 ~一般事務のオレがプログラミングさせられてる件について。~

解決したいこと

kintoneでのPDF出力が上手くいかない!

html2pdf.bundle.min.js ライブラリを使って、#printArea のHTML内容をPDFとして出力しています。出力はできるのですが、見切れてしまったり、線が太かったりする。

①の画像のPDF出力を押下すると②の画像のPDFが出力されます。
①の画像の更新を押すと①で表示されている用にHTMLが表示されます。
①の画像の更新を押したときのものがそのままPDFにできるのがベスト。


キャプチャ.PNG

キャaプチャ.PNG

kintoneにつくってあるアプリ日報から値を引っ張ってきて表示する出面というアプリを作ってます。
うえ

該当するソースコード

(function () {
  const EVENTS = ['app.record.create.show', 'app.record.edit.show'];
  const NIPPO_APP_ID = 〇〇;

  const FIELD_MTUK1 = 'mtuk1';//元請け名
  const FIELD_KIKAN1 = 'kikan1';
  const FIELD_KIKAN2 = 'kikan2';
  const OUTPUT_SPACE = 'output_table';//kintoneのスペースというフィールドに割り当ててるフィールドコード

  kintone.events.on(EVENTS, function (event) {
    const spaceEl = kintone.app.record.getSpaceElement(OUTPUT_SPACE);
    if (!spaceEl) return;

    spaceEl.innerHTML = `
      <style>
        #printArea {
          overflow-x: auto;
          font-family: 'Yu Gothic', '游ゴシック', 'serif';
          font-size: 10px;
        }
        .modern-button {
          background-color: #1976d2;
          color: white;
          border: none;
          padding: 6px 14px;
          font-size: 13px;
          border-radius: 5px;
          cursor: pointer;
          margin-right: 8px;
        }
        .modern-button:hover {
          background-color: #1565c0;
        }
        table {
          border-collapse: collapse;
          font-size: 10px;
        }
        th, td {
          border: 1px solid #333;
          padding: 2px 5px;
          text-align: center;
          white-space: nowrap;
          min-width: 40px;
        }
        h2 {
          text-align: center;
          font-weight: bold;
          font-size: 18px;
          letter-spacing: 2px;
        }
        h3 {
          margin: 20px 0 10px;
          font-size: 13px;
        }
        th.no-border {
          border: none;
          width: 100px;
        }
      </style>

      <button id="loadDataBtn" class="modern-button">更新</button>
      <button id="pdfBtn" class="modern-button">PDF出力</button>
      <div id="tableArea" style="margin-top:10px;"><div id="printArea"></div></div>
    `;

    document.getElementById('loadDataBtn').addEventListener('click', async () => {
      const record = kintone.app.record.get().record;
      const mtuk1 = record[FIELD_MTUK1].value;
      const kikan1 = record[FIELD_KIKAN1].value;
      const kikan2 = record[FIELD_KIKAN2].value;

      if (!mtuk1 || !kikan1 || !kikan2) {
        alert('元請け名と期間をすべて入力してください');
        return;
      }

      const query = [
        `mtuk = "${mtuk1}"`,
        `hiduke >= "${kikan1}"`,
        `hiduke <= "${kikan2}"`
      ].join(' and ');

      const result = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
        app: NIPPO_APP_ID,
        query: query,
        fields: ['hiduke', 'genbaname', 'teburu']
      });

      const records = result.records;

      const start = new Date(kikan1);
      const end = new Date(kikan2);
      const dateList = [];
      while (start <= end) {
        dateList.push(new Date(start));
        start.setDate(start.getDate() + 1);
      }

      const dataMap = {};
      const grandTotal = {};

      records.forEach(rec => {
        const dateStr = rec.hiduke.value;
        const date = new Date(dateStr).toISOString().split('T')[0];
        const site = rec.genbaname.value;
        const rows = rec.teburu.value;

        if (!dataMap[site]) dataMap[site] = {};

        rows.forEach(row => {
          const kousyu = row.value.kousyu.value || '不明';
          const ninku = parseFloat(row.value.ninku2.value || '0');
          const sabizan = parseFloat(row.value.sabizan2.value || '0');
          const sinya = parseFloat(row.value.sinya?.value || '0');

          if (!dataMap[site][kousyu]) {
            dataMap[site][kousyu] = {};
            dateList.forEach(d => {
              const dStr = d.toISOString().split('T')[0];
              dataMap[site][kousyu][dStr] = { ninku: 0, sabizan: 0, sinya: 0 };
            });
          }

          if (!grandTotal[kousyu]) {
            grandTotal[kousyu] = {};
            dateList.forEach(d => {
              const dStr = d.toISOString().split('T')[0];
              grandTotal[kousyu][dStr] = { ninku: 0, sabizan: 0, sinya: 0 };
            });
          }

          if (dataMap[site][kousyu][date]) {
            dataMap[site][kousyu][date].ninku += ninku;
            dataMap[site][kousyu][date].sabizan += sabizan;
            dataMap[site][kousyu][date].sinya += sinya;

            grandTotal[kousyu][date].ninku += ninku;
            grandTotal[kousyu][date].sabizan += sabizan;
            grandTotal[kousyu][date].sinya += sinya;
          }
        });
      });

      const printArea = document.getElementById('printArea');
      printArea.innerHTML = '';

      const heading = document.createElement('div');
      heading.innerHTML = `
        <div style="text-align:center; font-weight:bold; font-size:18px; margin-bottom: 5px;"><内訳書></div>
        <div style="display:flex; justify-content:space-between; margin-bottom: 10px;">
          <div style="font-weight:bold; font-size:14px;">${mtuk1} 御中</div>
          <div style="font-weight:bold;">${formatDate(kikan1)}${formatDate(kikan2)}</div>
        </div>
        <table>
          <tr>
            <th class="no-border"></th>
            <th class="no-border"></th>
            ${dateList.map(d => `<th>${d.getDate()}日</th>`).join('')}
            <th>合計</th>
          </tr>
        </table>
      `;
      printArea.appendChild(heading);

      for (const site of Object.keys(dataMap)) {
        const h3 = document.createElement('h3');
        h3.textContent = `◆ ${site}`;
        printArea.appendChild(h3);

        const table = document.createElement('table');
        const tbody = document.createElement('tbody');

        for (const kousyu of Object.keys(dataMap[site])) {
          ['ninku', 'sabizan', 'sinya'].forEach(type => {
            const row = document.createElement('tr');
            if (type === 'ninku') {
              row.innerHTML = `<td rowspan="3">${kousyu}</td><td>人工</td>`;
            } else if (type === 'sabizan') {
              row.innerHTML = `<td>早残</td>`;
            } else {
              row.innerHTML = `<td>深夜</td>`;
            }

            let total = 0;
            dateList.forEach(d => {
              const dStr = d.toISOString().split('T')[0];
              const val = dataMap[site][kousyu][dStr][type] || 0;
              total += val;
              row.innerHTML += `<td>${val === 0 ? '' : val.toFixed(1)}</td>`;
            });

            const unit = type === 'ninku' ? '人工' : '時間';
            row.innerHTML += `<td>${total.toFixed(1)} ${unit}</td>`;

            tbody.appendChild(row);
          });
        }

        table.appendChild(tbody);
        printArea.appendChild(table);
      }

      const totalTable = document.createElement('div');
      totalTable.innerHTML = `<h3>◆ 全体合計</h3>`;
      const t = document.createElement('table');
      const tb = document.createElement('tbody');

      for (const kousyu of Object.keys(grandTotal)) {
        const ninkuRow = document.createElement('tr');
        const sabizanRow = document.createElement('tr');
        const sinyaRow = document.createElement('tr');

        let totalNinku = 0;
        let totalSabizan = 0;
        let totalSinya = 0;

        ninkuRow.innerHTML = `<td rowspan="3">${kousyu}</td><td>人工</td>`;
        sabizanRow.innerHTML = `<td>早残</td>`;
        sinyaRow.innerHTML = `<td>深夜</td>`;

        dateList.forEach(d => {
          const dStr = d.toISOString().split('T')[0];
          const n = grandTotal[kousyu][dStr]?.ninku || 0;
          const s = grandTotal[kousyu][dStr]?.sabizan || 0;
          const sy = grandTotal[kousyu][dStr]?.sinya || 0;

          totalNinku += n;
          totalSabizan += s;
          totalSinya += sy;

          ninkuRow.innerHTML += `<td>${n ? n.toFixed(1) : ''}</td>`;
          sabizanRow.innerHTML += `<td>${s ? s.toFixed(1) : ''}</td>`;
          sinyaRow.innerHTML += `<td>${sy ? sy.toFixed(1) : ''}</td>`;
        });

        ninkuRow.innerHTML += `<td>${totalNinku.toFixed(1)} 人工</td>`;
        sabizanRow.innerHTML += `<td>${totalSabizan.toFixed(1)} 時間</td>`;
        sinyaRow.innerHTML += `<td>${totalSinya.toFixed(1)} 時間</td>`;

        tb.appendChild(ninkuRow);
        tb.appendChild(sabizanRow);
        tb.appendChild(sinyaRow);
      }

      t.appendChild(tb);
      totalTable.appendChild(t);
      printArea.appendChild(totalTable);

      const printInfo = document.createElement('div');
      const today = new Date();
      printInfo.innerHTML = `
        <div style="margin-top:20px; display:flex; justify-content:space-between; font-size:10px;">
          <div>${today.getFullYear()}${today.getMonth() + 1}${today.getDate()}日 株式会社〇〇</div>
        </div>
      `;
      printArea.appendChild(printInfo);

      window._scaleFactor = Math.max(1.3, 3 - dateList.length * 0.05);
    });

    document.getElementById('pdfBtn').addEventListener('click', () => {
      const element = document.getElementById('printArea');
      const opt = {
        margin: 5,
        filename: '内訳書.pdf',
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { scale: window._scaleFactor || 2 },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'landscape' }
      };
      html2pdf().set(opt).from(element).save();
    });

    function formatDate(isoStr) {
      const d = new Date(isoStr);
      return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}日`;
    }

    return event;
  });
})();

回答お願い致します。
タスケテ

0 likes

2Answer

色々複合的に原因があるように思えます。一つずつ調査して原因を解明していくと良いです。

PDFで表示が見切れることについて、scaleが固定だと、DOM幅とPDFページサイズが合わず、見切れの原因になることがあります。テーブル幅に応じてscaleを自動調整することで、良くなるかもです

// 元のスケール計算が固定すぎるので柔軟に調整
window._scaleFactor = Math.max(1, Math.min(2, 297 / element.scrollWidth));

こんな感じでテーブル幅に応じたスケーリング処理の見直しをするか、強制的に縮小率を下げて対応するのがいいと思います。
補足:A4横 = 約1123px として、比を取って調整する

線が太くなる件について、
html2canvasはborderの太さや縮小倍率に影響されるのと、「border:1px solid」がscale倍拡大され、太くレンダリングされている可能性があります。

CSSでborderを0.5pxに調整(PDFで1px相当に見える)するか、scaleを小さめに設定(先ほどの動的スケーリングを併用)してみてください。

th, td {
  border: 0.5px solid #333;
  /* 他はそのまま */
}

あとは、A4に合わせたmax-widthの指定をすると綺麗になるかもしれません。

#printArea {
  max-width: 1120px; /* A4横サイズからマージンを引いた幅 */
}
2Like

印刷エリア全体を表示したいけど、切れてしまっているのが問題なんだろうと思います。
Windows copilotに聞いてみました
下記のようにしてみてはいかがでしょうか

        #printArea {
+         width: 100%;
          overflow-x: auto;
          font-family: 'Yu Gothic', '游ゴシック', 'serif';
          font-size: 10px;
        }
1Like

Your answer might help someone💌