新增清潔費欄位 {{清潔費}},並記憶每份範本上次使用的值

- 表單新增「清潔費」欄位,預設 1200
- 後端 PDF 產生成功後自動將 monthlyRent / paymentDay / deposit / cleaningFee
  寫入 templates/defaults.json,key 為範本檔名
- 切換範本時呼叫 /api/templates/:name/defaults 自動帶入上次的值
- 表單版面調整為 2x2:每月租金/繳款日期、保證金/清潔費 各一行
- 4 份逢甲範本加入 {{清潔費}} 佔位符

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 14:17:31 +08:00
parent ef70576f4b
commit bfd978f7e6
8 changed files with 72 additions and 2 deletions
+20 -1
View File
@@ -23,7 +23,10 @@ document.getElementById('leaseStart').addEventListener('change', (e) => {
schedulePreview();
});
templateSelect.addEventListener('change', () => loadPreview());
templateSelect.addEventListener('change', () => {
loadTemplateDefaults(templateSelect.value);
loadPreview();
});
let previewTimer = null;
function schedulePreview() {
@@ -50,6 +53,7 @@ form.addEventListener('submit', async (event) => {
monthlyRent: formData.get('monthlyRent'),
paymentDay: formData.get('paymentDay'),
deposit: formData.get('deposit'),
cleaningFee: formData.get('cleaningFee'),
leaseStart: formData.get('leaseStart'),
leaseEnd: formData.get('leaseEnd'),
}),
@@ -119,6 +123,7 @@ async function loadTemplates() {
}
connectionStatus.textContent = '可使用';
loadTemplateDefaults(templateSelect.value);
loadPreview();
} catch (error) {
templateSelect.append(new Option('讀取失敗', ''));
@@ -129,6 +134,19 @@ async function loadTemplates() {
}
}
async function loadTemplateDefaults(name) {
if (!name) return;
try {
const res = await fetch(`/api/templates/${encodeURIComponent(name)}/defaults`);
if (!res.ok) return;
const defaults = await res.json();
if (defaults.monthlyRent) form.elements.monthlyRent.value = defaults.monthlyRent;
if (defaults.paymentDay) form.elements.paymentDay.value = defaults.paymentDay;
if (defaults.deposit) form.elements.deposit.value = defaults.deposit;
if (defaults.cleaningFee) form.elements.cleaningFee.value = defaults.cleaningFee;
} catch {}
}
let previewGen = 0;
let previewAbortController = null;
let previewBlobUrl = null;
@@ -151,6 +169,7 @@ async function loadPreview() {
monthlyRent: formData.get('monthlyRent') || '',
paymentDay: formData.get('paymentDay') || '',
deposit: formData.get('deposit') || '',
cleaningFee: formData.get('cleaningFee') || '',
leaseStart: formData.get('leaseStart') || '',
leaseEnd: formData.get('leaseEnd') || '',
});
+5 -1
View File
@@ -50,7 +50,7 @@
</div>
<div class="tool-panel fields-panel">
<div class="field-grid">
<div class="field-grid field-grid--2">
<label class="field">
<span>每月租金</span>
<input name="monthlyRent" type="number" inputmode="numeric" min="1" step="1" value="8000" required>
@@ -63,6 +63,10 @@
<span>保證金</span>
<input name="deposit" type="number" inputmode="numeric" min="1" step="1" value="16000" required>
</label>
<label class="field">
<span>清潔費</span>
<input name="cleaningFee" type="number" inputmode="numeric" min="1" step="1" value="1200" required>
</label>
</div>
<div class="field-grid field-grid--2">
+18
View File
@@ -7,6 +7,8 @@ const {
getTemplatePreviewPdf,
saveTemplate,
deleteTemplate,
getTemplateDefaults,
saveTemplateDefaults,
} = require('./src/contractService');
const app = express();
@@ -61,6 +63,14 @@ app.get('/api/templates', async (_req, res, next) => {
}
});
app.get('/api/templates/:name/defaults', async (req, res, next) => {
try {
res.json(await getTemplateDefaults(req.params.name));
} catch (error) {
next(error);
}
});
app.get('/api/templates/:name/preview', async (req, res) => {
try {
const pdfBuffer = await getTemplatePreviewPdf(req.params.name, req.query);
@@ -82,10 +92,18 @@ app.post('/api/contracts/pdf', async (req, res) => {
monthlyRent: req.body.monthlyRent,
paymentDay: req.body.paymentDay,
deposit: req.body.deposit,
cleaningFee: req.body.cleaningFee,
leaseStart: req.body.leaseStart,
leaseEnd: req.body.leaseEnd,
});
await saveTemplateDefaults(req.body.template, {
monthlyRent: req.body.monthlyRent,
paymentDay: req.body.paymentDay,
deposit: req.body.deposit,
cleaningFee: req.body.cleaningFee,
}).catch(() => {});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader(
'Content-Disposition',
+29
View File
@@ -20,6 +20,7 @@ const PLACEHOLDERS = [
['{{每月租金}}', 'monthlyRent', '每月租金', true],
['{{繳款日期}}', 'paymentDay', '繳款日期', true],
['{{保證金}}', 'deposit', '保證金', true],
['{{清潔費}}', 'cleaningFee', '清潔費', true],
['{{租賃開始年}}', 'leaseStartYear', '租賃開始年', false],
['{{租賃開始月}}', 'leaseStartMonth', '租賃開始月', false],
['{{租賃開始日}}', 'leaseStartDay', '租賃開始日', false],
@@ -84,6 +85,7 @@ function normalizeContractInput(input) {
monthlyRent: normalizePositiveInteger(input.monthlyRent, '每月租金'),
paymentDay: normalizePaymentDay(input.paymentDay),
deposit: normalizePositiveInteger(input.deposit, '保證金'),
cleaningFee: normalizePositiveInteger(input.cleaningFee, '清潔費'),
leaseStartYear: String(start.rocYear),
leaseStartMonth: String(start.month),
leaseStartDay: String(start.day),
@@ -362,6 +364,7 @@ function buildPreviewValues(input) {
if (input.monthlyRent) values.monthlyRent = String(input.monthlyRent);
if (input.paymentDay) values.paymentDay = String(input.paymentDay);
if (input.deposit) values.deposit = String(input.deposit);
if (input.cleaningFee) values.cleaningFee = String(input.cleaningFee);
if (input.leaseStart && /^\d{4}-\d{2}-\d{2}$/.test(input.leaseStart)) {
const [y, m, d] = input.leaseStart.split('-').map(Number);
@@ -424,6 +427,30 @@ async function getTemplatePreviewPdf(templateName, input = {}) {
}
}
const DEFAULTS_FILE = path.resolve(TEMPLATE_DIR, 'defaults.json');
const DEFAULT_CLEANING_FEE = '1200';
async function getTemplateDefaults(templateName) {
try {
const raw = await fs.readFile(DEFAULTS_FILE, 'utf-8');
const all = JSON.parse(raw);
return { cleaningFee: DEFAULT_CLEANING_FEE, ...all[templateName] };
} catch {
return { cleaningFee: DEFAULT_CLEANING_FEE };
}
}
async function saveTemplateDefaults(templateName, values) {
let all = {};
try {
const raw = await fs.readFile(DEFAULTS_FILE, 'utf-8');
all = JSON.parse(raw);
} catch {}
all[templateName] = { ...all[templateName], ...values };
await fs.mkdir(TEMPLATE_DIR, { recursive: true });
await fs.writeFile(DEFAULTS_FILE, JSON.stringify(all, null, 2), 'utf-8');
}
async function saveTemplate(originalName, buffer) {
const name = path.basename(String(originalName));
if (!name) throwClientError('檔名不正確。');
@@ -447,4 +474,6 @@ module.exports = {
getTemplatePreviewPdf,
saveTemplate,
deleteTemplate,
getTemplateDefaults,
saveTemplateDefaults,
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.