Tööriistad

<!DOCTYPE html>
<html lang="et">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Laenukalkulaator – annuiteetgraafik</title>
  <meta name="description" content="Ilus ja mugav laenu annuiteetgraafiku kalkulaator. Sisesta laenusumma, aastaintress, periood ning alguskuupäev – saad kuumakse ja täieliku maksegraafiku.">
  <style>
    :root {
      --bg: #0b1020;
      --card: #12182b;
      --card-2: #0f1530;
      --text: #e8ecf6;
      --muted: #9fb0d3;
      --accent: #6aa6ff;
      --accent-2: #7cf7d4;
      --border: #263252;
      --ring: rgba(106,166,255,.5);
      --danger: #ff6a6a;
    }
    * { box-sizing: border-box; }
    html, body { height: 100%; }
    body {
      margin: 0;
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
      color: var(--text);
      background: radial-gradient(1250px 650px at 10% -10%, #102046 0, transparent 60%),
                  radial-gradient(900px 550px at 90% -10%, #093c38 0, transparent 60%),
                  var(--bg);
      line-height: 1.45;
    }
    .container {
      max-width: 1100px;
      margin: 40px auto;
      padding: 0 16px;
    }
    header h1 {
      font-size: clamp(24px, 4vw, 40px);
      margin: 0 0 8px 0;
      letter-spacing: .3px;
    }
    header p {
      margin: 0;
      color: var(--muted);
    }
    .grid {
      display: grid;
      grid-template-columns: 1.2fr 1fr;
      gap: 20px;
      margin-top: 24px;
    }
    @media (max-width: 980px) {
      .grid { grid-template-columns: 1fr; }
    }
    .card {
      background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,0));
      border: 1px solid var(--border);
      border-radius: 16px;
      padding: 18px;
      box-shadow: 0 10px 30px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.03);
      backdrop-filter: blur(6px);
    }
    .card h2 {
      margin: 0 0 12px 0;
      font-size: 18px;
      color: #dbe6ff;
    }
    .row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
      margin-bottom: 12px;
    }
    .row-3 {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr;
      gap: 12px;
      margin-bottom: 12px;
    }
    @media (max-width: 640px) {
      .row, .row-3 { grid-template-columns: 1fr; }
    }
    label {
      display: block;
      font-size: 12px;
      color: var(--muted);
      margin: 0 0 6px;
    }
    input[type="text"], input[type="number"], input[type="date"] {
      width: 100%;
      padding: 12px 12px;
      border-radius: 12px;
      border: 1px solid var(--border);
      background: var(--card);
      color: var(--text);
      outline: none;
      transition: box-shadow .2s, border-color .2s;
    }
    input:focus {
      border-color: var(--accent);
      box-shadow: 0 0 0 4px var(--ring);
    }
    .hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
    .switch {
      display: inline-flex; gap: 8px; align-items: center; font-size: 12px; color: var(--muted);
      user-select: none;
    }
    .btns { display: flex; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
    button {
      appearance: none;
      border: 1px solid var(--border);
      background: linear-gradient(180deg, #1b2a4f, #10234a);
      color: var(--text);
      padding: 10px 14px;
      border-radius: 12px;
      cursor: pointer;
      font-weight: 600;
      letter-spacing: .2px;
      box-shadow: 0 6px 16px rgba(0,0,0,.25);
      transition: transform .03s ease, border-color .2s;
    }
    button:hover { border-color: var(--accent); }
    button:active { transform: translateY(1px); }
    .btn-secondary {
      background: var(--card);
    }
    .btn-danger {
      background: linear-gradient(180deg, #50262e, #3a1a1f);
      border-color: #6b2a34;
    }
    .stats {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
    }
    @media (max-width: 820px) { .stats { grid-template-columns: 1fr; } }
    .stat {
      background: var(--card-2);
      border: 1px solid var(--border);
      border-radius: 14px;
      padding: 14px;
    }
    .stat .label { color: var(--muted); font-size: 12px; }
    .stat .value { font-size: 22px; font-weight: 700; margin-top: 2px; }
    .table-wrap {
      margin-top: 14px;
      border: 1px solid var(--border);
      border-radius: 14px;
      overflow: hidden;
    }
    .table-head {
      display: grid;
      grid-template-columns: 70px 120px 1fr 1fr 1fr 1fr 1fr;
      padding: 10px 12px;
      background: #0f1630;
      border-bottom: 1px solid var(--border);
      font-size: 12px;
      color: #c9d8ff;
      position: sticky; top: 0; z-index: 1;
    }
    .row-item {
      display: grid;
      grid-template-columns: 70px 120px 1fr 1fr 1fr 1fr 1fr;
      padding: 10px 12px;
      border-bottom: 1px solid #1e2a4a;
      background: rgba(8, 14, 36, .6);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
      font-size: 13px;
    }
    .row-item:nth-child(odd) { background: rgba(8, 14, 36, .85); }
    .table {
      max-height: 420px;
      overflow: auto;
      scrollbar-width: thin;
    }
    .footer {
      display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
      padding: 10px 12px; background: #0f1630; border-top: 1px solid var(--border);
    }
    .badge {
      font-size: 12px; color: #a6c1ff;
      border: 1px dashed var(--border);
      padding: 6px 10px; border-radius: 999px;
      background: rgba(106,166,255,.08);
    }
    .notice { color: var(--muted); font-size: 12px; }
    .error { color: var(--danger); font-size: 12px; margin-top: 6px; }
    .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; }
    .grid-cols-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
    @media (max-width: 700px){ .grid-cols-3{ grid-template-columns: 1fr; } }
    .kbd { font-family: ui-monospace, Menlo, Consolas, monospace; background: #0c153a; border: 1px solid var(--border); font-size: 12px; padding: 1px 6px; border-radius: 6px; }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>Laenukalkulaator <span class="badge">annuiteetgraafik</span></h1>
      <p>Arvuta kuumakse ning vaata kogu graafikut. Sisendväljad: <em>Laenusumma (€), Aastaintress (%), Laenu pikkus (aastad / kuudes), Alguskuupäev</em>.</p>
    </header>

    <div class="grid">
      <!-- Vasak: sisendid -->
      <section class="card" aria-labelledby="inputs-title">
        <h2 id="inputs-title">Sisendid</h2>
        <div class="row">
          <div>
            <label for="amount">Laenusumma (€)</label>
            <input id="amount" type="text" inputmode="decimal" placeholder="nt 47 500" value="47 500" autocomplete="off">
            <div class="hint">Võid kasutada tühikuid või koma – puhastame automaatselt.</div>
          </div>
          <div>
            <label for="rate">Aastaintress (%)</label>
            <input id="rate" type="text" inputmode="decimal" placeholder="nt 9" value="9" autocomplete="off">
          </div>
        </div>
        <div class="row">
          <div>
            <label for="years">Laenu pikkus (aastad)</label>
            <input id="years" type="number" inputmode="numeric" placeholder="nt 5" value="5" min="0" step="1">
          </div>
          <div>
            <label for="months">Laenu pikkus (kuudes) <span class="hint">(jäta tühjaks kui kasutad aastaid)</span></label>
            <input id="months" type="number" inputmode="numeric" placeholder="nt 60" min="0" step="1">
          </div>
        </div>
        <div class="row">
          <div>
            <label for="start">Alguskuupäev</label>
            <input id="start" type="date">
            <div class="hint">Kui jätad tühjaks, alustame tänasest kuust.</div>
          </div>
          <div>
            <label for="rounding">Summa ümardus</label>
            <select id="rounding" style="width:100%; padding:12px; border-radius:12px; border:1px solid var(--border); background: var(--card); color: var(--text);">
              <option value="2" selected>Sentid (2 kohta)</option>
              <option value="0">Täiseuro</option>
            </select>
            <div class="hint">Ainult kuvamine – arvutused on täpsed.</div>
          </div>
        </div>

        <div class="btns">
          <button id="calcBtn">Arvuta graafik</button>
          <button id="resetBtn" class="btn-secondary">Lähtesta</button>
          <button id="csvBtn" class="btn-secondary">Ekspordi CSV</button>
          <button id="copyLinkBtn" class="btn-secondary">Jaga linki</button>
          <span id="error" class="error" role="alert" aria-live="polite"></span>
        </div>
      </section>

      <!-- Parem: kokkuvõte -->
      <section class="card" aria-labelledby="summary-title">
        <h2 id="summary-title">Kokkuvõte</h2>
        <div class="stats">
          <div class="stat">
            <div class="label">Kuumakse</div>
            <div id="outPayment" class="value">–</div>
          </div>
          <div class="stat">
            <div class="label">Kokku intress</div>
            <div id="outInterest" class="value">–</div>
          </div>
          <div class="stat">
            <div class="label">Tagasimaksete kogusumma</div>
            <div id="outTotal" class="value">–</div>
          </div>
        </div>

        <div class="hint" style="margin-top:10px">
          Valem: annuiteet <span class="kbd">M = P·r/(1-(1+r)^-n)</span>, kus <span class="kbd">r</span> on kuuintress <span class="kbd">aastaintress/12</span> ja <span class="kbd">n</span> kuude arv.
        </div>
      </section>
    </div>

    <!-- Tabel -->
    <section class="card" style="margin-top:20px" aria-labelledby="table-title">
      <h2 id="table-title">Maksegraafik</h2>
      <div class="table-wrap">
        <div class="table-head">
          <div>Nr</div>
          <div>Kuupäev</div>
          <div>Algjääk</div>
          <div>Intress</div>
          <div>Põhiosa</div>
          <div>Makse</div>
          <div>Lõppjääk</div>
        </div>
        <div id="tableBody" class="table" role="table" aria-label="Annuiteetgraafiku read"></div>
        <div class="footer">
          <span class="notice">NB! See on illustratiivne kalkulaator – tehingutingimused sõltuvad krediidiandjast.</span>
        </div>
      </div>
    </section>
  </div>

  <script>
    // Utiliidid
    const € = new Intl.NumberFormat('et-EE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 2 });
    const fmt = (x, d) => {
      if (!isFinite(x)) return "–";
      const n = typeof d === 'number' ? d : 2;
      return x.toLocaleString('et-EE', { minimumFractionDigits: n, maximumFractionDigits: n });
    };
    const parseMoney = (s) => {
      if (typeof s === 'number') return s;
      if (!s) return NaN;
      // Eemalda tühikud, koma -> punkt
      s = s.replace(/\s+/g, '').replace(',', '.');
      return parseFloat(s);
    };
    const parsePercent = (s) => parseMoney(s);

    const byId = (id) => document.getElementById(id);

    function monthsBetween(startDate, n) {
      const d = new Date(startDate.getTime());
      d.setHours(12,0,0,0); // väldi suve-/talveaja hüppeid
      d.setMonth(d.getMonth() + n);
      return d;
    }

    function buildSchedule(P, annualRatePct, nMonths, startDate, roundingDigits) {
      const rows = [];
      const r = (annualRatePct / 100) / 12;
      let payment;
      if (r === 0) {
        payment = P / nMonths;
      } else {
        payment = P * r / (1 - Math.pow(1 + r, -nMonths));
      }
      let balance = P;
      let date = new Date(startDate.getFullYear(), startDate.getMonth(), Math.min(startDate.getDate(), 28)); // väldi kuu lõpu eripärasid

      let totalInterest = 0;

      for (let i = 1; i <= nMonths; i++) {
        const interest = r === 0 ? 0 : balance * r;
        const principal = payment - interest;
        const endBalance = balance - principal;

        rows.push({
          nr: i,
          date: new Date(date.getTime()),
          begin: balance,
          interest,
          principal,
          pay: payment,
          end: endBalance < 1e-8 ? 0 : endBalance
        });

        totalInterest += interest;
        balance = endBalance;

        // Järgmine kuu
        date = monthsBetween(date, 1);
      }

      // Väike korrigeerimine viimases reas (ujukoma)
      const last = rows[rows.length - 1];
      if (last && Math.abs(last.end) < 0.01) {
        last.principal += last.end;
        last.end = 0;
      }

      const totalPay = payment * nMonths;
      return { rows, payment, totalInterest, totalPay, roundingDigits };
    }

    function renderSchedule(model) {
      const body = byId('tableBody');
      body.innerHTML = '';
      const d = model.roundingDigits;

      const frag = document.createDocumentFragment();
      for (const row of model.rows) {
        const div = document.createElement('div');
        div.className = 'row-item';
        div.innerHTML = `
          <div>${row.nr}</div>
          <div>${row.date.toLocaleDateString('et-EE')}</div>
          <div>${fmt(row.begin, d)}</div>
          <div>${fmt(row.interest, d)}</div>
          <div>${fmt(row.principal, d)}</div>
          <div>${fmt(row.pay, d)}</div>
          <div>${fmt(row.end, d)}</div>
        `;
        frag.appendChild(div);
      }
      body.appendChild(frag);
      byId('outPayment').textContent = €.format(model.payment);
      byId('outInterest').textContent = €.format(model.totalInterest);
      byId('outTotal').textContent = €.format(model.totalPay);
    }

    function toCSV(model) {
      const d = model.roundingDigits;
      const lines = [
        ['Nr','Kuupäev','Algjääk','Intress','Põhiosa','Makse','Lõppjääk'].join(';')
      ];
      for (const r of model.rows) {
        lines.push([
          r.nr,
          r.date.toLocaleDateString('et-EE'),
          fmt(r.begin, d),
          fmt(r.interest, d),
          fmt(r.principal, d),
          fmt(r.pay, d),
          fmt(r.end, d)
        ].join(';'));
      }
      return lines.join('\n');
    }

    function readInputs() {
      const amount = parseMoney(byId('amount').value);
      const rate = parsePercent(byId('rate').value);
      const years = byId('years').value ? parseInt(byId('years').value, 10) : 0;
      const monthsField = byId('months').value ? parseInt(byId('months').value, 10) : 0;
      const rounding = parseInt(byId('rounding').value, 10);
      let months = monthsField > 0 ? monthsField : (years > 0 ? years * 12 : 0);

      const startStr = byId('start').value;
      let start;
      if (startStr) {
        const [y,m,d] = startStr.split('-').map(Number);
        start = new Date(y, m-1, d);
      } else {
        const now = new Date();
        start = new Date(now.getFullYear(), now.getMonth(), 1);
      }
      return { amount, rate, months, start, rounding };
    }

    function validate({amount, rate, months}) {
      if (!isFinite(amount) || amount <= 0) return 'Palun sisesta korrektne laenusumma.';
      if (!isFinite(rate) || rate < 0) return 'Palun sisesta korrektne aastaintress (≥ 0%).';
      if (!Number.isInteger(months) || months <= 0) return 'Palun sisesta periood aastates või kuudes.';
      return '';
    }

    function saveToHash(inputs) {
      const params = new URLSearchParams();
      params.set('a', String(inputs.amount));
      params.set('r', String(inputs.rate));
      params.set('m', String(inputs.months));
      params.set('s', inputs.start.toISOString().slice(0,10));
      params.set('d', String(inputs.rounding));
      location.hash = params.toString();
    }

    function readFromHash() {
      if (!location.hash) return null;
      const q = new URLSearchParams(location.hash.slice(1));
      const amount = parseFloat(q.get('a'));
      const rate = parseFloat(q.get('r'));
      const months = parseInt(q.get('m') || '0', 10);
      const dstr = q.get('s');
      const rounding = parseInt(q.get('d') || '2', 10);
      if (!amount || !months) return null;
      const [y,m,d] = (dstr || '2025-09-01').split('-').map(Number);
      const start = new Date(y, m-1, d);
      return { amount, rate, months, start, rounding };
    }

    // Sündmused
    let currentModel = null;

    function computeAndRender() {
      const inputs = readInputs();
      const msg = validate(inputs);
      const errEl = byId('error');
      if (msg) { errEl.textContent = msg; return; }
      errEl.textContent = '';
      const model = buildSchedule(inputs.amount, inputs.rate, inputs.months, inputs.start, inputs.rounding);
      currentModel = model;
      renderSchedule(model);
      saveToHash(inputs);
    }

    byId('calcBtn').addEventListener('click', computeAndRender);
    byId('resetBtn').addEventListener('click', () => {
      byId('amount').value = '47 500';
      byId('rate').value = '9';
      byId('years').value = '5';
      byId('months').value = '';
      byId('start').value = '';
      byId('rounding').value = '2';
      byId('error').textContent = '';
      currentModel = null;
      byId('tableBody').innerHTML = '';
      byId('outPayment').textContent = '–';
      byId('outInterest').textContent = '–';
      byId('outTotal').textContent = '–';
      location.hash = '';
    });

    byId('csvBtn').addEventListener('click', () => {
      if (!currentModel) { computeAndRender(); }
      if (!currentModel) return;
      const blob = new Blob([toCSV(currentModel)], { type: 'text/csv;charset=utf-8' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'laenugraafik.csv';
      document.body.appendChild(a);
      a.click();
      a.remove();
    });

    byId('copyLinkBtn').addEventListener('click', async () => {
      const inputs = readInputs();
      const msg = validate(inputs);
      if (msg) { byId('error').textContent = msg; return; }
      saveToHash(inputs);
      const url = location.href;
      try {
        await navigator.clipboard.writeText(url);
        byId('error').textContent = 'Link kopeeritud lõikelauale!';
        setTimeout(()=>{ byId('error').textContent = ''; }, 2000);
      } catch {
        byId('error').textContent = 'Ei saanud linki kopeerida – kopeeri aadressiribalt käsitsi.';
      }
    });

    // Initsialiseeri väljad tänase kuuga või URL hashist
    (function initFromHashOrDefault(){
      const h = readFromHash();
      const startInput = byId('start');
      const today = new Date();
      const defaultDate = new Date(today.getFullYear(), today.getMonth(), 1);
      const iso = defaultDate.toISOString().slice(0,10);
      startInput.value = iso;

      if (h) {
        byId('amount').value = fmt(h.amount, 2);
        byId('rate').value = String(h.rate);
        byId('years').value = '';
        byId('months').value = String(h.months);
        byId('start').value = h.start.toISOString().slice(0,10);
        byId('rounding').value = String(h.rounding);
        const model = buildSchedule(h.amount, h.rate, h.months, h.start, h.rounding);
        currentModel = model;
        renderSchedule(model);
      }
    })();
  </script>
</body>
</html>
Scroll to Top
0

Subtotal