新增清潔費欄位 {{清潔費}},並記憶每份範本上次使用的值
- 表單新增「清潔費」欄位,預設 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:
+20
-1
@@ -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
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user