kintone上で表示されてるHTMLをPDF出力 ~一般事務のオレがプログラミングさせられてる件について。~
解決したいこと
kintoneでのPDF出力が上手くいかない!
html2pdf.bundle.min.js ライブラリを使って、#printArea のHTML内容をPDFとして出力しています。出力はできるのですが、見切れてしまったり、線が太かったりする。
①の画像のPDF出力を押下すると②の画像のPDFが出力されます。
①の画像の更新を押すと①で表示されている用にHTMLが表示されます。
①の画像の更新を押したときのものがそのままPDFにできるのがベスト。
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;
});
})();
回答お願い致します。
タスケテ