const form = document.querySelector('#contractForm'); const templateSelect = document.querySelector('#template'); const submitButton = document.querySelector('#submitButton'); const downloadButton = document.querySelector('#downloadButton'); const shareButton = document.querySelector('#shareButton'); const message = document.querySelector('#message'); const resultTitle = document.querySelector('#resultTitle'); const connectionStatus = document.querySelector('#connectionStatus'); const previewFrame = document.querySelector('#previewFrame'); const previewLoading = document.querySelector('#previewLoading'); let currentPdfBlob = null; let currentPdfFileName = '租屋契約.pdf'; loadTemplates(); setDefaultLeaseDates(); document.getElementById('leaseStart').addEventListener('change', (e) => { const [y, m, d] = e.target.value.split('-').map(Number); if (!y || !m || !d) return; document.getElementById('leaseEnd').value = toDateInputValue(computeLeaseEnd(new Date(y, m - 1, d))); schedulePreview(); }); templateSelect.addEventListener('change', () => loadPreview()); let previewTimer = null; function schedulePreview() { clearTimeout(previewTimer); previewTimer = setTimeout(loadPreview, 600); } form.querySelectorAll('input').forEach((el) => el.addEventListener('input', schedulePreview)); form.addEventListener('submit', async (event) => { event.preventDefault(); setBusy(true); resetPdf(); try { const formData = new FormData(form); const response = await fetch('/api/contracts/pdf', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ template: formData.get('template'), monthlyRent: formData.get('monthlyRent'), paymentDay: formData.get('paymentDay'), deposit: formData.get('deposit'), leaseStart: formData.get('leaseStart'), leaseEnd: formData.get('leaseEnd'), }), }); if (!response.ok) { const errorBody = await response.json().catch(() => ({})); throw new Error(errorBody.error || 'PDF 產生失敗。'); } currentPdfBlob = await response.blob(); currentPdfFileName = getFileNameFromDisposition(response.headers.get('Content-Disposition')) || buildDefaultFileName(formData.get('template')); resultTitle.textContent = 'PDF 已產生'; message.textContent = currentPdfFileName; downloadButton.disabled = false; shareButton.disabled = !canShareCurrentPdf(); } catch (error) { resultTitle.textContent = '產生失敗'; message.textContent = error.message; } finally { setBusy(false); } }); downloadButton.addEventListener('click', () => { if (!currentPdfBlob) return; downloadPdf(); }); shareButton.addEventListener('click', async () => { if (!currentPdfBlob) return; const file = new File([currentPdfBlob], currentPdfFileName, { type: 'application/pdf' }); if (!navigator.canShare || !navigator.canShare({ files: [file] })) { downloadPdf(); return; } try { await navigator.share({ title: '租屋契約 PDF', files: [file] }); } catch (error) { if (error.name !== 'AbortError') { message.textContent = '分享失敗,已保留下載按鈕。'; } } }); async function loadTemplates() { try { const response = await fetch('/api/templates'); if (!response.ok) throw new Error('無法讀取範本。'); const body = await response.json(); templateSelect.replaceChildren( ...body.templates.map((name) => new Option(name, name)), ); if (body.templates.length === 0) { templateSelect.append(new Option('templates 資料夾沒有範本', '')); templateSelect.disabled = true; submitButton.disabled = true; connectionStatus.textContent = '缺少範本'; message.textContent = '請先將 .docx 範本放進 templates 資料夾。'; return; } connectionStatus.textContent = '可使用'; loadPreview(); } catch (error) { templateSelect.append(new Option('讀取失敗', '')); templateSelect.disabled = true; submitButton.disabled = true; connectionStatus.textContent = '離線'; message.textContent = error.message; } } let previewGen = 0; let previewAbortController = null; let previewBlobUrl = null; async function loadPreview() { const name = templateSelect.value; if (!name) { previewFrame.src = 'about:blank'; previewLoading.hidden = true; return; } if (previewAbortController) previewAbortController.abort(); previewAbortController = new AbortController(); const { signal } = previewAbortController; const gen = ++previewGen; const formData = new FormData(form); const params = new URLSearchParams({ monthlyRent: formData.get('monthlyRent') || '', paymentDay: formData.get('paymentDay') || '', deposit: formData.get('deposit') || '', leaseStart: formData.get('leaseStart') || '', leaseEnd: formData.get('leaseEnd') || '', }); previewLoading.hidden = false; try { const res = await fetch( `/api/templates/${encodeURIComponent(name)}/preview?${params}`, { signal }, ); if (!res.ok) throw new Error('preview failed'); const blob = await res.blob(); if (gen !== previewGen) return; if (previewBlobUrl) URL.revokeObjectURL(previewBlobUrl); previewBlobUrl = URL.createObjectURL(blob); previewFrame.src = `${previewBlobUrl}#toolbar=0`; } catch { // AbortError 或其他錯誤都忽略,交給 finally 處理 } finally { if (gen === previewGen) previewLoading.hidden = true; } } function setBusy(isBusy) { submitButton.disabled = isBusy || templateSelect.disabled; submitButton.textContent = isBusy ? '產生中' : '產生 PDF'; } function resetPdf() { currentPdfBlob = null; downloadButton.disabled = true; shareButton.disabled = true; resultTitle.textContent = '產生中'; message.textContent = '正在建立 PDF。'; } function canShareCurrentPdf() { if (!currentPdfBlob || !navigator.canShare) return false; const file = new File([currentPdfBlob], currentPdfFileName, { type: 'application/pdf' }); return navigator.canShare({ files: [file] }); } function downloadPdf() { const url = URL.createObjectURL(currentPdfBlob); const link = document.createElement('a'); link.href = url; link.download = currentPdfFileName; document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function getFileNameFromDisposition(disposition) { if (!disposition) return ''; const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i); if (utf8Match) return decodeURIComponent(utf8Match[1]); const asciiMatch = disposition.match(/filename="([^"]+)"/i); return asciiMatch ? asciiMatch[1] : ''; } function buildDefaultFileName(templateName) { const baseName = String(templateName || '租屋契約').replace(/\.[^.]+$/, ''); return `${baseName}.pdf`; } function setDefaultLeaseDates() { const start = new Date(); start.setDate(1); start.setMonth(start.getMonth() + 1); document.getElementById('leaseStart').value = toDateInputValue(start); document.getElementById('leaseEnd').value = toDateInputValue(computeLeaseEnd(start)); } function computeLeaseEnd(startDate) { const end = new Date(startDate); end.setFullYear(end.getFullYear() + 1); end.setDate(end.getDate() - 1); return end; } function toDateInputValue(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; }