Загрузка данных


const app = document.getElementById("app");

const APP_TITLE = "Greenbank";
const AUTH_KEY = "greenbank-authenticated";
let passwordResetCode = null;

const services = [
  {
    title: "Перевод по номеру карты",
    text: "Быстрый платеж между счетами Greenbank и в сторонние банки.",
    icon: "⇄",
  },
  {
    title: "Оплата услуг",
    text: "Коммунальные, мобильная связь, штрафы и регулярные шаблоны.",
    icon: "◫",
  },
  {
    title: "Инвестиционный перевод",
    text: "Покупка фондов, округление трат и откладывание на цели.",
    icon: "↗",
  },
];

let bankCards = [];
let bankStats = {
  username: "",
  login_attempts: 0,
  successful_logins: 0,
  failed_logins: 0,
  password_changes: 0,
  last_login_at: null,
  last_password_change_at: null,
};

let toastTimer = null;

function formatRubles(value) {
  return new Intl.NumberFormat("ru-RU", {
    style: "currency",
    currency: "RUB",
    maximumFractionDigits: 2,
  }).format(value);
}

function formatForeign(value, currency) {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
    maximumFractionDigits: 2,
  }).format(value);
}

function totalBalance() {
  return bankCards.reduce(
    (sum, card) =>
      sum + (card.currency === "RUB" ? card.balance : card.balance * 91.2),
    0,
  );
}

function apiJson(path, options = {}) {
  return fetch(path, {
    headers: {
      "Content-Type": "application/json",
      ...(options.headers || {}),
    },
    ...options,
  });
}

function formatTimestamp(value) {
  if (!value) {
    return "—";
  }

  return new Intl.DateTimeFormat("ru-RU", {
    dateStyle: "medium",
    timeStyle: "short",
  }).format(new Date(value * 1000));
}

async function loadBackendState() {
  try {
    const [stateResponse, statsResponse, cardsResponse] = await Promise.all([
      fetch("/api/state"),
      fetch("/api/auth/stats"),
      fetch("/api/auth/cards"),
    ]);

    if (stateResponse.ok) {
      const state = await stateResponse.json();
      const payload = state?.payload || {};

      if (payload.username) {
        bankStats.username = payload.username;
      }

      if (payload.stats) {
        bankStats = { ...bankStats, ...payload.stats };
      }
    }

    if (statsResponse.ok) {
      bankStats = { ...bankStats, ...(await statsResponse.json()) };
    }

    if (cardsResponse.ok) {
      bankCards = await cardsResponse.json();
    }
  } catch {
    // ignore
  }
}

async function refreshStats() {
  try {
    const response = await fetch("/api/auth/stats");
    if (response.ok) {
      bankStats = { ...bankStats, ...(await response.json()) };
    }
  } catch {
    // Keep the last known snapshot.
  }
}

function getRoute() {
  const path = window.location.pathname.replace(/\/+$/, "") || "/";
  return ["/", "/login", "/reset", "/dashboard"].includes(path) ? path : "/";
}

function isAuthenticated() {
  return sessionStorage.getItem(AUTH_KEY) === "1";
}

function setAuthenticated(value) {
  if (value) {
    sessionStorage.setItem(AUTH_KEY, "1");
  } else {
    sessionStorage.removeItem(AUTH_KEY);
  }
}

function navigate(path) {
  if (window.location.pathname !== path) {
    window.history.pushState({}, "", path);
  }
  render();
  window.scrollTo({ top: 0, behavior: "smooth" });
}

function showToast(message) {
  let toast = document.querySelector(".toast");
  if (!toast) {
    toast = document.createElement("div");
    toast.className = "toast";
    document.body.appendChild(toast);
  }

  toast.textContent = message;
  toast.classList.add("is-visible");

  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => {
    toast.classList.remove("is-visible");
  }, 2200);
}

function topbar(activePath) {
  const navItems = [
    ["/", "Главная"],
    ["/login", "Вход"],
  ];

  const nav = navItems
    .map(
      ([path, label]) =>
        `<a class="nav-link ${path === activePath ? "active" : ""}" href="${path}" data-nav="${path}">${label}</a>`,
    )
    .join("");

  return `
    <header class="topbar">
      <div class="brand">
        <a class="no-no-no-link" href="/">
          <div class="brand-mark">G</div>
        </a>
        <div class="brand-text">
          <div class="brand-title">Greenbank</div>
          <div class="brand-subtitle">Личный банк для ежедневных операций</div>
        </div>
      </div>
      <nav class="topbar-nav" aria-label="Основная навигация">
        ${nav}
      </nav>
    </header>
  `;
}

function landingView() {
  return `
    ${topbar("/")}
    <section class="layout-grid landing-layout">
      <article class="hero">
          <span class="eyebrow">Цифровой сервис для частных клиентов</span>
          <h1>Удобный банк для повседневных операций.</h1>
          <p class="hero-copy">
            Greenbank предлагает единый доступ к основным банковским сервисам, личному кабинету и управлению картами.
            Интерфейс ориентирован на быстрый вход и понятную навигацию по ключевым операциям.
          </p>
          <div class="hero-actions">
            <button class="primary-button" data-action="go-login">Войти в кабинет</button>
          </div>
        <div class="hero-stats">
          <div class="stat-card">
            <div class="muted">Среднее время отклика</div>
            <span class="stat-value">0,4 c</span>
          </div>
          <div class="stat-card">
            <div class="muted">Клиентов на обслуживании</div>
            <span class="stat-value">0M</span>
          </div>
          <div class="stat-card">
            <div class="muted">Доступных сервисов</div>
            <span class="stat-value">24</span>
          </div>
        </div>
      </article>

      <aside class="hero-aside">
        <div class="panel">
          <span class="pill">AI-included</span>
          <h2>Банк, опережающий время</h2>
          <p class="panel-copy">Комбинируем привычную банковскую структуру с современными технологиями.</p>
          <ul class="hero-list">
            <li>Работа в более чем в 1 странах мира</li>
            <li>Безосновательные блокировки счетов</li>
            <li>Оплата ухмылкой</li>
            <li>Первый банк без живых сотрудников</li>
            <li>Мод на много денег</li>
          </ul>
        </div>
      </aside>
    </section>

    <section class="panel" style="margin-top: 20px;">
      <div class="panel-top">
        <div>
          <div class="panel-kicker">Сервисы банка</div>
          <h2 class="section-title">Основные возможности личного кабинета</h2>
        </div>
      </div>
      <div class="features-grid">
        <article class="feature-card">
          <div class="feature-icon">▣</div>
          <h3 class="feature-title">Карты и счета</h3>
          <p class="feature-copy">Просмотр балансов, реквизитов и статуса счетов в одном разделе.</p>
        </article>
        <article class="feature-card">
          <div class="feature-icon">◉</div>
          <h3 class="feature-title">Платежи и переводы</h3>
          <p class="feature-copy">Операции между своими счетами, по реквизитам и по номеру карты.</p>
        </article>
        <article class="feature-card">
          <div class="feature-icon">✦</div>
          <h3 class="feature-title">Уведомления и поддержка</h3>
          <p class="feature-copy">Контроль операций, сообщения по важным событиям и доступ к сервисной информации.</p>
        </article>
      </div>
    </section>
  `;
}

function loginView(message = "") {
  return `
    ${topbar("/login")}
    <section class="form-shell">
      <article class="hero form-panel">
        <span class="eyebrow">Доступ для клиента</span>
        <h1 class="form-title">Войдите в Greenbank</h1>
        <p class="form-copy">Введите учётные данные для доступа в личный кабинет и работы с картами.</p>
      </article>

      <article class="glass-card form-card">
        <div>
          <div class="panel-kicker">Авторизация</div>
          <h2 class="form-title">Введите логин и пароль</h2>
          <p class="form-copy">Если забыли пароль, воспользуйтесь формой восстановления доступа.</p>
        </div>
        ${message ? `<div class="notice notice--warning">${message}</div>` : ""}
        <form id="login-form" class="field-grid">
          <div class="field">
            <label for="login">Логин</label>
            <input id="login" name="login" type="text" autocomplete="username" placeholder="greenbank" required />
          </div>
          <div class="field">
            <label for="password">Пароль</label>
            <input id="password" name="password" type="password" autocomplete="current-password" placeholder="Введите пароль" required />
          </div>
          <div class="form-actions">
            <button class="primary-button" type="submit">Войти</button>
            <button class="ghost-button" type="button" data-action="go-reset">Забыли пароль?</button>
          </div>
        </form>
        <div class="auth-footer">
          <span class="fineprint">Если возникли вопросы, обратитесь в службу поддержки. Они вам <i>обязательно</i> помогут</span>
          <button class="link-button" type="button" data-action="go-home">Вернуться на главную</button>
        </div>
      </article>
    </section>
  `;
}

function resetView(message = "") {
  const codeRequested = typeof passwordResetCode === "string";

  return `
    ${topbar("/reset")}
    <section class="form-shell">
      <article class="hero form-panel">
        <span class="eyebrow">Восстановление доступа</span>
        <h1 class="form-title">Сброс пароля</h1>
        <p class="form-copy">Для восстановления доступа используется код подтверждения, отправляемый на указанный номер. После успешного подтверждения вы сможете задать новый пароль.</p>
      </article>

      <article class="glass-card form-card">
        <div>
          <h2 class="form-title">Задайте новый пароль</h2>
          <p class="form-copy">Сначала запросите код на номер телефона, затем введите его и новый пароль для аккаунта Greenbank.</p>
        </div>
        ${message ? `<div class="notice">${message}</div>` : ""}
        <form id="reset-form" class="field-grid">
          <div class="field">
            <label for="phone">Телефон</label>
            <input id="phone" name="phone" type="text" autocomplete="tel" placeholder="+7 (999) 123-45-67" required />
          </div>
          ${
            codeRequested
              ? `
          <div class="notice notice--warning">Код отправлен на указанный номер. Введите его ниже.</div>
          <div class="field">
            <label for="code">SMS-код</label>
            <input id="code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" placeholder="000000" required />
          </div>
          `
              : `
          <div class="field field--inline">
            <label class="sr-only" for="request-code">Получить код</label>
            <button class="primary-button" type="button" id="request-code">Получить код</button>
            <span class="field-hint">После нажатия появится поле для ввода SMS-кода.</span>
          </div>
          `
          }
          <div class="field">
            <label for="new-password">Новый пароль</label>
            <input id="new-password" name="new-password" type="password" autocomplete="new-password" placeholder="Придумайте новый пароль" required />
          </div>
          <div class="form-actions">
            <button class="primary-button" type="submit" ${codeRequested ? "" : "disabled"}>Подтвердить</button>
            <button class="ghost-button" type="button" data-action="go-login">У меня уже есть пароль</button>
          </div>
        </form>
        <div class="auth-footer">
          <button class="link-button" type="button" data-action="go-home">На главную</button>
        </div>
      </article>
    </section>
  `;
}

function dashboardView() {
  const total = totalBalance();
  const cards = bankCards;

  return `
    ${topbar("/dashboard")}
    <section class="layout-grid dashboard-layout">
      <article class="panel summary-card">
        <div class="summary-top">
          <div>
            <div class="panel-kicker">Личный кабинет</div>
            <h1 class="dashboard-title">Добрый день, ${bankStats.username}</h1>
            <p class="dashboard-copy">Здесь отображаются несколько карт, общий баланс и быстрые финансовые операции.</p>
          </div>
          <div class="auth-actions">
            <button class="secondary-button" type="button" data-action="logout">Выйти</button>
          </div>
        </div>

        <div class="accent-line"></div>

        <div class="balance">
          <div class="balance-label">Общий баланс по всем картам</div>
          <div class="balance-amount">${formatRubles(total)}</div>
          <div class="meta">Сумма рассчитана по курсу кабинета и обновляется на лету.</div>
        </div>

        <div class="hero-stats" style="margin-top: 20px;">
          <div class="stat-card">
            <div class="muted">Активные карты</div>
            <span class="stat-value">${cards.length}</span>
          </div>
          <div class="stat-card">
            <div class="muted">Свободный лимит</div>
            <span class="stat-value">${formatRubles(120000)}</span>
          </div>
          <div class="stat-card">
            <div class="muted">Защита</div>
            <span class="stat-value">3D-Sec</span>
          </div>
        </div>

        <div class="panel-stack" style="margin-top: 20px;">
          <div class="panel-top">
            <div>
              <div class="panel-kicker">Карты</div>
              <h2 class="section-title">Баланс по нескольким счетам</h2>
            </div>
          </div>
          <div class="account-list">
            ${cards
              .map(
                (card, index) => `
                  <article class="account-card ${index === 0 ? "account-card--accent" : ""}">
                    <div class="account-top">
                      <div>
                        <div class="account-icon">${index === 0 ? "◈" : index === 1 ? "◎" : "◇"}</div>
                        <h3 class="feature-title" style="margin-top: 12px;">${card.name}</h3>
                        <div class="account-number">${card.number}</div>
                      </div>
                      <span class="pill">${card.status}</span>
                    </div>
                    <div class="account-balance">${card.currency === "RUB" ? formatRubles(card.balance) : formatForeign(card.balance, card.currency)}</div>
                    <div class="account-meta">
                      <span class="pill pill--muted">Greenbank</span>
                      <span class="pill pill--muted">Пластиковая</span>
                    </div>
                  </article>
                `,
              )
              .join("")}
          </div>
        </div>
      </article>

      <aside class="dashboard-grid">
        <div class="panel">
          <div class="service-top">
            <div>
              <div class="panel-kicker">Операции</div>
              <h2 class="section-title">Доступные действия</h2>
            </div>
          </div>
          <div class="service-list">
            ${services
              .map(
                (service) => `
                  <button class="action-tile service-button" type="button" data-action="mock-service" data-service="${service.title}">
                    <div class="service-row">
                      <div class="service-icon">${service.icon}</div>
                      <div>
                        <h3 class="service-title">${service.title}</h3>
                        <p class="service-copy">${service.text}</p>
                      </div>
                    </div>
                  </button>
                `,
              )
              .join("")}
          </div>
        </div>
      </aside>
    </section>
  `;
}

function render() {
  const route = getRoute();

  if (route === "/dashboard" && !isAuthenticated()) {
    window.history.replaceState({}, "", "/login");
    app.innerHTML = loginView("Сначала войдите в систему.");
    document.title = `${APP_TITLE} · Вход`;
    bindCurrentView();
    return;
  }

  if (route === "/") {
    app.innerHTML = landingView();
    document.title = `${APP_TITLE} · Главная`;
  } else if (route === "/login") {
    app.innerHTML = loginView();
    document.title = `${APP_TITLE} · Вход`;
  } else if (route === "/reset") {
    app.innerHTML = resetView();
    document.title = `${APP_TITLE} · Сброс пароля`;
  } else {
    app.innerHTML = dashboardView();
    document.title = `${APP_TITLE} · Кабинет`;
  }

  bindCurrentView();
}

async function bootstrap() {
  await loadBackendState();
  render();
}

function bindCurrentView() {
  document.querySelectorAll("[data-nav]").forEach((link) => {
    link.addEventListener("click", (event) => {
      event.preventDefault();
      navigate(link.getAttribute("data-nav"));
    });
  });

  document.querySelectorAll("[data-action]").forEach((control) => {
    control.addEventListener("click", () => {
      const action = control.getAttribute("data-action");
      if (action === "go-login") {
        navigate("/login");
      }
      if (action === "go-reset") {
        passwordResetCode = null;
        navigate("/reset");
      }
      if (action === "go-home") {
        navigate("/");
      }
      if (action === "logout") {
        setAuthenticated(false);
        showToast("Сеанс завершён");
        navigate("/login");
      }
      if (action === "mock-service") {
        const service = control.getAttribute("data-service") || "Операция";
        showToast(
          `Мы маленькая инди-компания, функционал пока в разработке :c`,
        );
      }
    });
  });

  const loginForm = document.getElementById("login-form");
  if (loginForm) {
    loginForm.addEventListener("submit", async (event) => {
      event.preventDefault();
      const formData = new FormData(loginForm);
      const login = String(formData.get("login") || "").trim();
      const password = String(formData.get("password") || "");

      try {
        const response = await apiJson("/api/auth/login", {
          method: "POST",
          body: JSON.stringify({ username: login, password }),
        });
        const result = await response.json();

        if (response.ok && result.success) {
          setAuthenticated(result.token);
          await refreshStats();
          showToast("Вход выполнен");
          navigate("/dashboard");
          return;
        }

        showToast(result.message || "Неверный логин или пароль");
      } catch {
        showToast("Не удалось выполнить вход");
      }
    });
  }

  const resetForm = document.getElementById("reset-form");
  if (resetForm) {
    const requestCodeButton = document.getElementById("request-code");
    if (requestCodeButton) {
      requestCodeButton.addEventListener("click", async () => {
        const phoneInput = document.getElementById("phone");
        const phone = String(phoneInput?.value || "").trim();

        if (phone === "") {
          showToast("Введите номер телефона для получения кода");
          return;
        }

        console.log(phone);

        const response = await fetch("/api/auth/change-password/request-code", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ phone }),
        });

        if (response.status !== 200) {
          showToast("Не удалось запросить код. Проверьте правильность логина.");
        }

        const result = await response.json();

        passwordResetCode = result.code;
        showToast("Код отправлен по SMS");
        render();
      });
    }

    resetForm.addEventListener("submit", async (event) => {
      event.preventDefault();
      if (!passwordResetCode) {
        showToast("Сначала получите SMS-код");
        return;
      }

      const formData = new FormData(resetForm);
      const phone = String(formData.get("phone") || "").trim();
      const code = String(formData.get("code") || "").trim();
      const newPassword = String(formData.get("new-password") || "").trim();

      if (phone === "") {
        showToast("Введите номер телефона");
        return;
      }

      if (code !== passwordResetCode) {
        showToast("Неверный SMS-код");
        return;
      }

      if (newPassword.length < 6) {
        showToast("Пароль должен быть не короче 6 символов");
        return;
      }

      try {
        const response = await apiJson("/api/auth/change-password", {
          method: "POST",
          body: JSON.stringify({
            phone,
            code: passwordResetCode,
            new_password: newPassword,
          }),
        });
        const result = await response.json();

        if (response.ok && result.success) {
          passwordResetCode = null;
          await refreshStats();
          showToast("Пароль обновлён");
          navigate("/login");
          return;
        }

        showToast(result.message || "Не удалось обновить пароль");
      } catch {
        showToast("Не удалось обновить пароль");
      }
    });
  }
}

window.addEventListener("popstate", render);
bootstrap();