住宅購入 資金計画書
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>住宅購入 資金計画書</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
<style>
body { font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif; }
input[type=text]::-webkit-inner-spin-button { -webkit-appearance: none; }
@media print {
.no-print { display: none !important; }
body { background: white; }
}
</style>
</head>
<body class="bg-slate-50">
<div id="root"></div>
<script type="text/babel">
const { useState, useMemo } = React;
function formatCurrency(value) {
if (!value && value !== 0) return '¥0';
return '¥' + Number(value).toLocaleString('ja-JP');
}
function toNum(strVal) {
if (strVal === '' || strVal === null || strVal === undefined) return 0;
const n = parseInt(String(strVal).replace(/,/g, ''), 10);
return isNaN(n) ? 0 : n;
}
function toDisplayStr(rawDigits) {
if (rawDigits === '') return '';
const n = parseInt(rawDigits, 10);
if (isNaN(n)) return '';
return n.toLocaleString('ja-JP');
}
/* ── 入力フィールド ── */
function InputField({ label, note, value, onChange, disabled }) {
return (
<div className={`mb-4 ${disabled ? 'opacity-40 pointer-events-none' : ''}`}>
<label className="flex items-baseline gap-2 text-sm font-medium text-slate-700 mb-1.5">
{label}
{note && <span className="text-xs font-normal text-slate-400">{note}</span>}
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm select-none">¥</span>
<input
type="text"
inputMode="numeric"
value={value}
onChange={onChange}
disabled={disabled}
placeholder="0"
className="w-full pl-7 pr-3 py-2.5 border border-slate-200 rounded-xl bg-white
text-right text-slate-800 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent
transition-shadow placeholder:text-slate-300"
/>
</div>
</div>
);
}
/* ── 費用フィールド(金額+支払先) ── */
function FeeField({ label, note, value, onChange, payee, onPayeeChange }) {
return (
<div className="mb-4">
<label className="flex items-baseline gap-2 text-sm font-medium text-slate-700 mb-1.5">
{label}
{note && <span className="text-xs font-normal text-slate-400">{note}</span>}
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm select-none">¥</span>
<input
type="text"
inputMode="numeric"
value={value}
onChange={onChange}
placeholder="0"
className="w-full pl-7 pr-3 py-2.5 border border-slate-200 rounded-xl bg-white
text-right text-slate-800 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent
transition-shadow placeholder:text-slate-300"
/>
</div>
<input
type="text"
value={payee}
onChange={onPayeeChange}
placeholder="支払先"
className="mt-1.5 w-full px-3 py-1.5 border border-slate-100 rounded-lg bg-slate-50
text-slate-600 text-xs
focus:outline-none focus:ring-1 focus:ring-blue-300 focus:border-transparent
transition-shadow placeholder:text-slate-300"
/>
</div>
);
}
/* ── 結果カード ── */
function ResultCard({ label, sub, value, variant = 'blue' }) {
const s = {
blue: { card: 'bg-blue-600 border-blue-600', label: 'text-blue-100', sub: 'text-blue-200', val: 'text-white' },
teal: { card: 'bg-teal-600 border-teal-600', label: 'text-teal-100', sub: 'text-teal-200', val: 'text-white' },
indigo: { card: 'bg-indigo-700 border-indigo-700', label: 'text-indigo-100', sub: 'text-indigo-200', val: 'text-white' },
violet: { card: 'bg-violet-600 border-violet-600', label: 'text-violet-100', sub: 'text-violet-200', val: 'text-white' },
}[variant];
return (
<div className={`rounded-xl p-4 border ${s.card}`}>
<p className={`text-xs font-semibold tracking-wide mb-0.5 ${s.label}`}>{label}</p>
{sub && <p className={`text-xs mb-1 ${s.sub}`}>{sub}</p>}
<p className={`text-xl font-bold tabular-nums ${s.val}`}>{formatCurrency(value)}</p>
</div>
);
}
/* ── 資金計画表の行 ── */
function TR({ label, value, level, total }) {
const labelCls = level === 'header'
? 'py-2 px-4 text-xs font-semibold text-slate-400 uppercase tracking-wider bg-slate-50'
: level === 'total'
? 'py-2.5 px-4 text-sm font-bold text-slate-800 bg-blue-50'
: level === 'sub'
? 'py-2 pl-8 pr-4 text-sm text-slate-500'
: level === 'note'
? 'py-1.5 pl-8 pr-4 text-xs text-slate-400 italic border-b border-slate-100'
: 'py-2 px-4 text-sm text-slate-700';
const valueCls = level === 'header'
? 'py-2 px-4 text-xs bg-slate-50'
: level === 'total'
? 'py-2.5 px-4 text-sm font-bold text-blue-700 text-right bg-blue-50 tabular-nums'
: 'py-2 px-4 text-sm text-slate-700 text-right tabular-nums';
if (level === 'header' || level === 'note') {
return (
<tr>
<td colSpan={2} className={labelCls}>{label}</td>
</tr>
);
}
return (
<tr className="border-b border-slate-100">
<td className={labelCls}>{label}</td>
<td className={valueCls}>{formatCurrency(value)}</td>
</tr>
);
}
/* ── テキスト入力フィールド ── */
function TextField({ label, value, onChange, placeholder }) {
return (
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 mb-1.5">{label}</label>
<input
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
className="w-full px-3 py-2.5 border border-slate-200 rounded-xl bg-white
text-slate-800 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent
transition-shadow placeholder:text-slate-300"
/>
</div>
);
}
/* ── メインアプリ ── */
function App() {
const [type, setType] = useState('house'); // 'house' | 'mansion'
const [customerName, setCustomerName] = useState('');
const [propertyName, setPropertyName] = useState('');
const [staffName, setStaffName] = useState('');
const [payees, setPayees] = useState({
agencyFeePayee: '', registrationFeePayee: '', stampDutyPayee: '',
officeFeePayee: '', fireInsurancePayee: '', propertyTaxPayee: '',
bankGuaranteeFeePayee: '', managementFeePayee: '', repairFundPayee: '',
});
const [f, setF] = useState({
purchasePrice: '',
downPayment: '',
agencyFee: '',
registrationFee: '',
stampDuty: '',
officeFee: '',
fireInsurance: '',
propertyTax: '',
bankGuaranteeFee: '',
managementFee: '',
repairFund: '',
});
const handleChange = (key) => (e) => {
const raw = e.target.value.replace(/[^0-9]/g, '');
setF(prev => ({ ...prev, [key]: toDisplayStr(raw) }));
};
const handlePayeeChange = (key) => (e) => {
setPayees(prev => ({ ...prev, [key]: e.target.value }));
};
const v = useMemo(() => {
const result = {};
for (const k of Object.keys(f)) result[k] = toNum(f[k]);
return result;
}, [f]);
const calc = useMemo(() => {
const miscCostsWithoutGuarantee =
v.agencyFee + v.registrationFee + v.stampDuty + v.officeFee + v.fireInsurance + v.propertyTax;
const miscCosts = miscCostsWithoutGuarantee + v.bankGuaranteeFee;
const remaining = Math.max(0, v.purchasePrice - v.downPayment);
const monthlyCosts = type === 'mansion' ? v.managementFee + v.repairFund : 0;
const settlementAmount = remaining + miscCosts;
const totalPayment = v.purchasePrice + miscCosts;
const suggestedBankGuarantee = Math.round((v.purchasePrice + miscCostsWithoutGuarantee) * 0.022 + 55000);
return { miscCosts, remaining, monthlyCosts, settlementAmount, totalPayment, suggestedBankGuarantee };
}, [v, type]);
const reset = () => {
setType('house');
setCustomerName('');
setPropertyName('');
setStaffName('');
setPayees({
agencyFeePayee: '', registrationFeePayee: '', stampDutyPayee: '',
officeFeePayee: '', fireInsurancePayee: '', propertyTaxPayee: '',
bankGuaranteeFeePayee: '', managementFeePayee: '', repairFundPayee: '',
});
setF({
purchasePrice: '',
downPayment: '',
agencyFee: '',
registrationFee: '',
stampDuty: '',
officeFee: '',
fireInsurance: '',
propertyTax: '',
bankGuaranteeFee: '',
managementFee: '',
repairFund: '',
});
};
const isMansion = type === 'mansion';
const downloadPDF = () => {
const el = document.getElementById('pdf-area');
const name = customerName ? `資金計画書_${customerName}` : (propertyName ? `資金計画書_${propertyName}` : '資金計画書');
// A4 1枚に収めるため、PDF非表示要素を一時的に隠す
const noPdfEls = el.querySelectorAll('[data-no-pdf]');
noPdfEls.forEach(e => { e._prevDisplay = e.style.display; e.style.display = 'none'; });
html2pdf().set({
margin: [8, 8, 8, 8],
filename: `${name}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
}).from(el).save().then(() => {
noPdfEls.forEach(e => { e.style.display = e._prevDisplay; });
});
};
const downloadExcel = () => {
const today = new Date().toLocaleDateString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit' });
const bdr = () => ({
top: { style: 'thin', color: { rgb: 'BBBBBB' } },
bottom: { style: 'thin', color: { rgb: 'BBBBBB' } },
left: { style: 'thin', color: { rgb: 'BBBBBB' } },
right: { style: 'thin', color: { rgb: 'BBBBBB' } },
});
const S = {
companyTitle: { font: { bold: true, sz: 14, color: { rgb: 'FFFFFF' } }, fill: { fgColor: { rgb: '3B4F6B' }, patternType: 'solid' }, alignment: { horizontal: 'center', vertical: 'center' }, border: bdr() },
companyInfo: { font: { sz: 9, color: { rgb: '555555' } }, fill: { fgColor: { rgb: 'EEE9E0' }, patternType: 'solid' }, alignment: { horizontal: 'center', vertical: 'center' }, border: bdr() },
docTitle: { font: { bold: true, sz: 15 }, alignment: { horizontal: 'center', vertical: 'center' } },
infoLabel: { font: { bold: true, sz: 10 }, fill: { fgColor: { rgb: 'F5F5F0' }, patternType: 'solid' }, border: bdr() },
infoValue: { font: { sz: 10 }, border: bdr() },
infoDate: { font: { sz: 10 }, border: bdr(), alignment: { horizontal: 'right' } },
colHeader: { font: { bold: true, sz: 10, color: { rgb: 'FFFFFF' } }, fill: { fgColor: { rgb: '4A6FA5' }, patternType: 'solid' }, alignment: { horizontal: 'center', vertical: 'center' }, border: bdr() },
section: { font: { bold: true, sz: 10, color: { rgb: '2E4D7B' } }, fill: { fgColor: { rgb: 'E0E8F5' }, patternType: 'solid' }, border: bdr() },
item: { font: { sz: 10 }, border: bdr() },
itemSub: { font: { sz: 10, color: { rgb: '555555' } }, border: bdr() },
payee: { font: { sz: 10 }, border: bdr(), alignment: { horizontal: 'center', vertical: 'center', wrapText: true } },
amount: { font: { sz: 10 }, border: bdr(), alignment: { horizontal: 'right' } },
subAmount: { font: { sz: 10, color: { rgb: '555555' } }, border: bdr(), alignment: { horizontal: 'right' } },
total: { font: { bold: true, sz: 10 }, fill: { fgColor: { rgb: 'DBE9FF' }, patternType: 'solid' }, border: bdr() },
totalAmt: { font: { bold: true, sz: 10 }, fill: { fgColor: { rgb: 'DBE9FF' }, patternType: 'solid' }, border: bdr(), alignment: { horizontal: 'right' } },
grandTotal: { font: { bold: true, sz: 11, color: { rgb: 'FFFFFF' } }, fill: { fgColor: { rgb: '2B5BA8' }, patternType: 'solid' }, border: bdr() },
grandTotalAmt:{ font: { bold: true, sz: 11, color: { rgb: 'FFFFFF' } }, fill: { fgColor: { rgb: '2B5BA8' }, patternType: 'solid' }, border: bdr(), alignment: { horizontal: 'right' } },
empty: {},
};
const wb = XLSX.utils.book_new();
const ws = {};
const merges = [];
const fmt = '¥#,##0';
const setC = (r, c, v, s, isNum) => {
const addr = XLSX.utils.encode_cell({ r, c });
ws[addr] = { v: v ?? '', s: s || S.empty, t: isNum ? 'n' : 's' };
if (isNum) ws[addr].z = fmt;
};
const setN = (r, c, v, s) => setC(r, c, v, s, true);
const setT = (r, c, v, s) => setC(r, c, v, s, false);
const mg = (r1,c1,r2,c2) => merges.push({ s:{r:r1,c:c1}, e:{r:r2,c:c2} });
setT(0,0,'センチュリー21 えびす不動産',S.companyTitle); setT(0,1,'',S.companyTitle); setT(0,2,'',S.companyTitle); mg(0,0,0,2);
setT(1,0,'〒596-0053 大阪府岸和田市沼町30番8号 TEL:072-437-1521 MAIL:info@21-ebisu.com',S.companyInfo); setT(1,1,'',S.companyInfo); setT(1,2,'',S.companyInfo); mg(1,0,1,2);
setT(3,0,'住 宅 購 入 資 金 計 画 書',S.docTitle); setT(3,1,'',S.docTitle); setT(3,2,'',S.docTitle); mg(3,0,3,2);
setT(5,0,'お客様名',S.infoLabel); setT(5,1,customerName||'',S.infoValue); setT(5,2,`作成日:${today}`,S.infoDate);
setT(6,0,'物件名', S.infoLabel); setT(6,1,propertyName||'',S.infoValue); setT(6,2,'',S.infoValue);
setT(7,0,'担当者', S.infoLabel); setT(7,1,staffName||'', S.infoValue); setT(7,2,type==='mansion'?'マンション':type==='land'?'土地':'戸建て',S.infoDate);
// 列ヘッダー:項目 | 金額 | 支払先
setT(9,0,'項 目',S.colHeader); setT(9,1,'金 額',S.colHeader); setT(9,2,'支 払 先',S.colHeader);
let r = 10;
setT(r,0,'■ 物件費用',S.section); setT(r,1,'',S.section); setT(r,2,'',S.section); mg(r,0,r,2); r++;
setT(r,0,'売買代金',S.item); setN(r,1,v.purchasePrice,S.amount); setT(r,2,'',S.payee); r++;
setT(r,0,' 手付金',S.itemSub); setN(r,1,v.downPayment,S.subAmount); setT(r,2,'',S.payee); r++;
setT(r,0,' 残代金(引渡し時)',S.itemSub); setN(r,1,calc.remaining,S.subAmount); setT(r,2,'',S.payee); r++;
setT(r,0,'■ 諸費用',S.section); setT(r,1,'',S.section); setT(r,2,'',S.section); mg(r,0,r,2); r++;
[
[' 仲介手数料', v.agencyFee, payees.agencyFeePayee],
[' 登記費用', v.registrationFee, payees.registrationFeePayee],
[' 印紙代', v.stampDuty, payees.stampDutyPayee],
[' ローン事務手数料', v.officeFee, payees.officeFeePayee],
[' 火災保険料', v.fireInsurance, payees.fireInsurancePayee],
[' 固定資産税', v.propertyTax, payees.propertyTaxPayee],
[' 銀行保証料', v.bankGuaranteeFee, payees.bankGuaranteeFeePayee],
].forEach(([lbl,amt,pee]) => { setT(r,0,lbl,S.itemSub); setN(r,1,amt,S.subAmount); setT(r,2,pee||'',S.payee); r++; });
setT(r,0,'諸費用合計',S.total); setN(r,1,calc.miscCosts,S.totalAmt); setT(r,2,'',S.total); r++;
if (isMansion) {
setT(r,0,'■ 月額費用(マンション)',S.section); setT(r,1,'',S.section); setT(r,2,'',S.section); mg(r,0,r,2); r++;
setT(r,0,' 管理費(月額)', S.itemSub); setN(r,1,v.managementFee,S.subAmount); setT(r,2,payees.managementFeePayee||'',S.payee); r++;
setT(r,0,' 修繕積立金(月額)',S.itemSub); setN(r,1,v.repairFund,S.subAmount); setT(r,2,payees.repairFundPayee||'',S.payee); r++;
setT(r,0,'月額費用合計',S.total); setN(r,1,calc.monthlyCosts,S.totalAmt); setT(r,2,'',S.total); r++;
}
r++;
setT(r,0,'決済時の支払額(手付金以外の費用)',S.grandTotal); setN(r,1,calc.settlementAmount,S.grandTotalAmt); setT(r,2,'',S.grandTotal); r++;
r++;
setT(r,0,'総支払い額(物件代金+諸費用+保証料)',S.grandTotal); setN(r,1,calc.totalPayment,S.grandTotalAmt); setT(r,2,'',S.grandTotal);
ws['!ref'] = XLSX.utils.encode_range({ s:{r:0,c:0}, e:{r:r,c:2} });
ws['!merges'] = merges;
ws['!cols'] = [{ wch: 30 }, { wch: 16 }, { wch: 20 }];
ws['!rows'] = [{ hpt: 30 }, { hpt: 16 }, { hpt: 10 }, { hpt: 26 }, { hpt: 10 }];
ws['!pageSetup'] = { paperSize: 9, orientation: 'portrait', fitToPage: true, fitToWidth: 1, fitToHeight: 1 };
ws['!margins'] = { left: 0.55, right: 0.55, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 };
XLSX.utils.book_append_sheet(wb, ws, '資金計画書');
const name = customerName ? `資金計画書_${customerName}` : (propertyName ? `資金計画書_${propertyName}` : '資金計画書');
XLSX.writeFile(wb, `${name}.xlsx`);
};
return (
<div className="min-h-screen bg-slate-50 pb-0">
{/* ヘッダー */}
<header className="bg-white border-b border-slate-200 shadow-sm">
<div className="max-w-5xl mx-auto px-4 py-5 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
<polyline strokeLinecap="round" strokeLinejoin="round" points="9 22 9 12 15 12 15 22" />
</svg>
</div>
<div>
<h1 className="text-xl font-bold text-slate-800 leading-tight">住宅購入 資金計画書</h1>
<p className="text-xs text-slate-400 mt-0.5">必要事項を入力すると費用が自動計算されます</p>
</div>
</div>
</header>
<div className="max-w-5xl mx-auto px-4 pt-6 grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* ── 左カラム:入力フォーム ── */}
<section className="lg:col-span-2 no-print">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<h2 className="text-base font-bold text-slate-700 mb-5 pb-3 border-b border-slate-100">
入力フォーム
</h2>
{/* お客様名・担当者 */}
<div className="mb-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">基本情報</p>
</div>
<TextField
label="お客様名"
value={customerName}
onChange={e => setCustomerName(e.target.value)}
placeholder="例:山田 太郎 様"
/>
<TextField
label="物件名"
value={propertyName}
onChange={e => setPropertyName(e.target.value)}
placeholder="例:〇〇マンション101号室"
/>
<TextField
label="担当者"
value={staffName}
onChange={e => setStaffName(e.target.value)}
placeholder="例:鈴木 花子"
/>
{/* 物件種別 */}
<div className="mb-6">
<p className="text-sm font-medium text-slate-700 mb-2">物件種別</p>
<div className="grid grid-cols-3 gap-2 p-1 bg-slate-100 rounded-xl">
{[
{ value: 'house', label: '戸建て' },
{ value: 'land', label: '土地' },
{ value: 'mansion', label: 'マンション' },
].map(opt => (
<button
key={opt.value}
onClick={() => setType(opt.value)}
className={`py-2 text-sm font-semibold rounded-lg transition-all ${
type === opt.value
? 'bg-white text-blue-600 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
{opt.label}
</button>
))}
</div>
</div>
{/* 物件費用 */}
<div className="mb-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">物件費用</p>
</div>
<InputField label="売買代金" value={f.purchasePrice} onChange={handleChange('purchasePrice')} />
<InputField label="手付金" value={f.downPayment} onChange={handleChange('downPayment')} />
{/* 諸費用 */}
<div className="mt-5 mb-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">諸費用</p>
</div>
<FeeField label="仲介手数料" value={f.agencyFee} onChange={handleChange('agencyFee')} payee={payees.agencyFeePayee} onPayeeChange={handlePayeeChange('agencyFeePayee')} />
<FeeField label="登記費用" value={f.registrationFee} onChange={handleChange('registrationFee')} payee={payees.registrationFeePayee} onPayeeChange={handlePayeeChange('registrationFeePayee')} />
<FeeField label="印紙代" value={f.stampDuty} onChange={handleChange('stampDuty')} payee={payees.stampDutyPayee} onPayeeChange={handlePayeeChange('stampDutyPayee')} />
<FeeField label="ローン事務手数料" value={f.officeFee} onChange={handleChange('officeFee')} payee={payees.officeFeePayee} onPayeeChange={handlePayeeChange('officeFeePayee')} />
<FeeField label="火災保険料" value={f.fireInsurance} onChange={handleChange('fireInsurance')} payee={payees.fireInsurancePayee} onPayeeChange={handlePayeeChange('fireInsurancePayee')} />
<FeeField label="固定資産税" value={f.propertyTax} onChange={handleChange('propertyTax')} payee={payees.propertyTaxPayee} onPayeeChange={handlePayeeChange('propertyTaxPayee')} />
<p className="text-xs text-slate-400 italic -mt-2 mb-4 pl-1">
4/1〜3/31までの日割精算
</p>
{/* マンション専用:月額費用 */}
{isMansion && (
<>
<div className="mt-5 mb-2">
<p className="text-xs font-semibold text-blue-400 uppercase tracking-wider mb-3">月額費用(マンション)</p>
</div>
<FeeField label="管理費" note="月額" value={f.managementFee} onChange={handleChange('managementFee')} payee={payees.managementFeePayee} onPayeeChange={handlePayeeChange('managementFeePayee')} />
<FeeField label="修繕積立金" note="月額" value={f.repairFund} onChange={handleChange('repairFund')} payee={payees.repairFundPayee} onPayeeChange={handlePayeeChange('repairFundPayee')} />
</>
)}
{/* 銀行保証料(最後) */}
<div className="mt-5 mb-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">銀行保証料</p>
</div>
<FeeField label="銀行保証料" value={f.bankGuaranteeFee} onChange={handleChange('bankGuaranteeFee')} payee={payees.bankGuaranteeFeePayee} onPayeeChange={handlePayeeChange('bankGuaranteeFeePayee')} />
<p className="text-xs text-blue-400 -mt-2 mb-4 pl-1 leading-relaxed">
参考: {formatCurrency(calc.suggestedBankGuarantee)}<br />
(売買代金+諸費用の2.2%+¥55,000)
</p>
<button
onClick={reset}
className="w-full mt-5 py-2.5 border-2 border-slate-200 text-slate-500 text-sm font-semibold
rounded-xl hover:bg-slate-50 hover:border-slate-300 active:scale-95 transition-all"
>
リセット
</button>
</div>
</section>
{/* ── 右カラム:結果 ── */}
<div className="lg:col-span-3 space-y-5">
{/* ダウンロードボタン */}
<div className="no-print flex gap-2 justify-end flex-wrap">
<button
onClick={downloadExcel}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl border-2 border-emerald-500 text-emerald-600 text-sm font-semibold hover:bg-emerald-50 active:scale-95 transition-all"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Excelダウンロード
</button>
<button
onClick={downloadPDF}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl border-2 border-blue-500 text-blue-600 text-sm font-semibold hover:bg-blue-50 active:scale-95 transition-all"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
PDFダウンロード
</button>
</div>
{/* PDF / 印刷エリア ここから */}
<div id="pdf-area" className="space-y-4">
{/* PDF ヘッダー:顧客情報 */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm px-5 py-3">
<div className="flex justify-between items-center mb-2 pb-2 border-b border-slate-100">
<h2 className="text-sm font-bold text-slate-800 tracking-wide">住宅購入 資金計画書</h2>
<p className="text-xs text-slate-400">{new Date().toLocaleDateString('ja-JP', {year:'numeric',month:'2-digit',day:'2-digit'})}</p>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-400 w-16 flex-shrink-0">お客様名</span>
<span className="text-sm font-bold text-slate-800">{customerName || '―'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-400 w-16 flex-shrink-0">担当者</span>
<span className="text-sm font-semibold text-slate-700">{staffName || '―'}</span>
</div>
<div className="flex items-center gap-2 col-span-2">
<span className="text-xs text-slate-400 w-16 flex-shrink-0">物件名</span>
<span className="text-sm font-semibold text-slate-700">{propertyName || '―'}</span>
</div>
</div>
</div>
{/* 計算結果カード群(画面表示のみ・PDF非表示) */}
<section data-no-pdf="true">
<h2 className="text-base font-bold text-slate-700 mb-3">計算結果</h2>
<div className="grid grid-cols-2 gap-3">
<ResultCard label="物件代金" sub="手付金+残代金" value={v.purchasePrice} variant="blue" />
<ResultCard label="諸費用合計" sub="仲介・登記・保険ほか" value={calc.miscCosts} variant="blue" />
{isMansion && (
<ResultCard label="月額費用" sub="管理費+修繕積立金" value={calc.monthlyCosts} variant="violet" />
)}
<ResultCard label="決済時の支払額" sub="手付金以外の費用" value={calc.settlementAmount} variant="teal" />
<ResultCard label="総支払い額" sub="物件代金+諸費用+保証料" value={calc.totalPayment} variant="indigo" />
</div>
</section>
{/* 資金計画表 */}
<section>
<h2 className="text-base font-bold text-slate-700 mb-3">資金計画表</h2>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<table className="w-full border-collapse">
<tbody>
<TR label="物件費用" level="header" />
<TR label="売買代金" value={v.purchasePrice} />
<TR label="手付金" value={v.downPayment} level="sub" />
<TR label="残代金(引渡し時)" value={calc.remaining} level="sub" />
<TR label="諸費用" level="header" />
<TR label="仲介手数料" value={v.agencyFee} level="sub" />
<TR label="登記費用" value={v.registrationFee} level="sub" />
<TR label="印紙代" value={v.stampDuty} level="sub" />
<TR label="ローン事務手数料" value={v.officeFee} level="sub" />
<TR label="火災保険料" value={v.fireInsurance} level="sub" />
<TR label="固定資産税" value={v.propertyTax} level="sub" />
<TR label="銀行保証料" value={v.bankGuaranteeFee} level="sub" />
<TR label="諸費用合計" value={calc.miscCosts} level="total" />
{isMansion && (
<>
<TR label="月額費用(マンション)" level="header" />
<TR label="管理費" value={v.managementFee} level="sub" />
<TR label="修繕積立金" value={v.repairFund} level="sub" />
<TR label="月額費用合計" value={calc.monthlyCosts} level="total" />
</>
)}
<TR label="決済時の支払額" level="header" />
<TR label="決済時の支払額(手付金以外の費用)" value={calc.settlementAmount} level="total" />
<TR label="総支払い額(物件代金+諸費用+保証料)" value={calc.totalPayment} level="total" />
</tbody>
</table>
</div>
</section>
</div>{/* /pdf-area */}
</div>
</div>
{/* フッター */}
<footer className="bg-[#BEAF88] mt-12">
<div className="max-w-5xl mx-auto px-4 py-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div className="text-slate-800">
<p className="text-sm font-bold tracking-wide">センチュリー21 えびす不動産</p>
<p className="text-xs text-slate-700 mt-0.5">〒596-0053 大阪府岸和田市沼町30番8号</p>
</div>
<div className="flex flex-col sm:items-end text-xs text-slate-700 gap-0.5">
<span>TEL:072-437-1521</span>
<span>MAIL:info@21-ebisu.com</span>
<a href="https://www.21-ebisu.jp" target="_blank" rel="noopener noreferrer"
className="text-blue-800 hover:text-blue-900 underline underline-offset-2">
https://www.21-ebisu.jp
</a>
</div>
</div>
</footer>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>