新增清潔費欄位 {{清潔費}},並記憶每份範本上次使用的值
- 表單新增「清潔費」欄位,預設 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();
|
schedulePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
templateSelect.addEventListener('change', () => loadPreview());
|
templateSelect.addEventListener('change', () => {
|
||||||
|
loadTemplateDefaults(templateSelect.value);
|
||||||
|
loadPreview();
|
||||||
|
});
|
||||||
|
|
||||||
let previewTimer = null;
|
let previewTimer = null;
|
||||||
function schedulePreview() {
|
function schedulePreview() {
|
||||||
@@ -50,6 +53,7 @@ form.addEventListener('submit', async (event) => {
|
|||||||
monthlyRent: formData.get('monthlyRent'),
|
monthlyRent: formData.get('monthlyRent'),
|
||||||
paymentDay: formData.get('paymentDay'),
|
paymentDay: formData.get('paymentDay'),
|
||||||
deposit: formData.get('deposit'),
|
deposit: formData.get('deposit'),
|
||||||
|
cleaningFee: formData.get('cleaningFee'),
|
||||||
leaseStart: formData.get('leaseStart'),
|
leaseStart: formData.get('leaseStart'),
|
||||||
leaseEnd: formData.get('leaseEnd'),
|
leaseEnd: formData.get('leaseEnd'),
|
||||||
}),
|
}),
|
||||||
@@ -119,6 +123,7 @@ async function loadTemplates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectionStatus.textContent = '可使用';
|
connectionStatus.textContent = '可使用';
|
||||||
|
loadTemplateDefaults(templateSelect.value);
|
||||||
loadPreview();
|
loadPreview();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
templateSelect.append(new Option('讀取失敗', ''));
|
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 previewGen = 0;
|
||||||
let previewAbortController = null;
|
let previewAbortController = null;
|
||||||
let previewBlobUrl = null;
|
let previewBlobUrl = null;
|
||||||
@@ -151,6 +169,7 @@ async function loadPreview() {
|
|||||||
monthlyRent: formData.get('monthlyRent') || '',
|
monthlyRent: formData.get('monthlyRent') || '',
|
||||||
paymentDay: formData.get('paymentDay') || '',
|
paymentDay: formData.get('paymentDay') || '',
|
||||||
deposit: formData.get('deposit') || '',
|
deposit: formData.get('deposit') || '',
|
||||||
|
cleaningFee: formData.get('cleaningFee') || '',
|
||||||
leaseStart: formData.get('leaseStart') || '',
|
leaseStart: formData.get('leaseStart') || '',
|
||||||
leaseEnd: formData.get('leaseEnd') || '',
|
leaseEnd: formData.get('leaseEnd') || '',
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-1
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-panel fields-panel">
|
<div class="tool-panel fields-panel">
|
||||||
<div class="field-grid">
|
<div class="field-grid field-grid--2">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>每月租金</span>
|
<span>每月租金</span>
|
||||||
<input name="monthlyRent" type="number" inputmode="numeric" min="1" step="1" value="8000" required>
|
<input name="monthlyRent" type="number" inputmode="numeric" min="1" step="1" value="8000" required>
|
||||||
@@ -63,6 +63,10 @@
|
|||||||
<span>保證金</span>
|
<span>保證金</span>
|
||||||
<input name="deposit" type="number" inputmode="numeric" min="1" step="1" value="16000" required>
|
<input name="deposit" type="number" inputmode="numeric" min="1" step="1" value="16000" required>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>清潔費</span>
|
||||||
|
<input name="cleaningFee" type="number" inputmode="numeric" min="1" step="1" value="1200" required>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-grid field-grid--2">
|
<div class="field-grid field-grid--2">
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const {
|
|||||||
getTemplatePreviewPdf,
|
getTemplatePreviewPdf,
|
||||||
saveTemplate,
|
saveTemplate,
|
||||||
deleteTemplate,
|
deleteTemplate,
|
||||||
|
getTemplateDefaults,
|
||||||
|
saveTemplateDefaults,
|
||||||
} = require('./src/contractService');
|
} = require('./src/contractService');
|
||||||
|
|
||||||
const app = express();
|
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) => {
|
app.get('/api/templates/:name/preview', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const pdfBuffer = await getTemplatePreviewPdf(req.params.name, req.query);
|
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,
|
monthlyRent: req.body.monthlyRent,
|
||||||
paymentDay: req.body.paymentDay,
|
paymentDay: req.body.paymentDay,
|
||||||
deposit: req.body.deposit,
|
deposit: req.body.deposit,
|
||||||
|
cleaningFee: req.body.cleaningFee,
|
||||||
leaseStart: req.body.leaseStart,
|
leaseStart: req.body.leaseStart,
|
||||||
leaseEnd: req.body.leaseEnd,
|
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-Type', 'application/pdf');
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
'Content-Disposition',
|
'Content-Disposition',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const PLACEHOLDERS = [
|
|||||||
['{{每月租金}}', 'monthlyRent', '每月租金', true],
|
['{{每月租金}}', 'monthlyRent', '每月租金', true],
|
||||||
['{{繳款日期}}', 'paymentDay', '繳款日期', true],
|
['{{繳款日期}}', 'paymentDay', '繳款日期', true],
|
||||||
['{{保證金}}', 'deposit', '保證金', true],
|
['{{保證金}}', 'deposit', '保證金', true],
|
||||||
|
['{{清潔費}}', 'cleaningFee', '清潔費', true],
|
||||||
['{{租賃開始年}}', 'leaseStartYear', '租賃開始年', false],
|
['{{租賃開始年}}', 'leaseStartYear', '租賃開始年', false],
|
||||||
['{{租賃開始月}}', 'leaseStartMonth', '租賃開始月', false],
|
['{{租賃開始月}}', 'leaseStartMonth', '租賃開始月', false],
|
||||||
['{{租賃開始日}}', 'leaseStartDay', '租賃開始日', false],
|
['{{租賃開始日}}', 'leaseStartDay', '租賃開始日', false],
|
||||||
@@ -84,6 +85,7 @@ function normalizeContractInput(input) {
|
|||||||
monthlyRent: normalizePositiveInteger(input.monthlyRent, '每月租金'),
|
monthlyRent: normalizePositiveInteger(input.monthlyRent, '每月租金'),
|
||||||
paymentDay: normalizePaymentDay(input.paymentDay),
|
paymentDay: normalizePaymentDay(input.paymentDay),
|
||||||
deposit: normalizePositiveInteger(input.deposit, '保證金'),
|
deposit: normalizePositiveInteger(input.deposit, '保證金'),
|
||||||
|
cleaningFee: normalizePositiveInteger(input.cleaningFee, '清潔費'),
|
||||||
leaseStartYear: String(start.rocYear),
|
leaseStartYear: String(start.rocYear),
|
||||||
leaseStartMonth: String(start.month),
|
leaseStartMonth: String(start.month),
|
||||||
leaseStartDay: String(start.day),
|
leaseStartDay: String(start.day),
|
||||||
@@ -362,6 +364,7 @@ function buildPreviewValues(input) {
|
|||||||
if (input.monthlyRent) values.monthlyRent = String(input.monthlyRent);
|
if (input.monthlyRent) values.monthlyRent = String(input.monthlyRent);
|
||||||
if (input.paymentDay) values.paymentDay = String(input.paymentDay);
|
if (input.paymentDay) values.paymentDay = String(input.paymentDay);
|
||||||
if (input.deposit) values.deposit = String(input.deposit);
|
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)) {
|
if (input.leaseStart && /^\d{4}-\d{2}-\d{2}$/.test(input.leaseStart)) {
|
||||||
const [y, m, d] = input.leaseStart.split('-').map(Number);
|
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) {
|
async function saveTemplate(originalName, buffer) {
|
||||||
const name = path.basename(String(originalName));
|
const name = path.basename(String(originalName));
|
||||||
if (!name) throwClientError('檔名不正確。');
|
if (!name) throwClientError('檔名不正確。');
|
||||||
@@ -447,4 +474,6 @@ module.exports = {
|
|||||||
getTemplatePreviewPdf,
|
getTemplatePreviewPdf,
|
||||||
saveTemplate,
|
saveTemplate,
|
||||||
deleteTemplate,
|
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