JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Untitled Page | JustPaste.app
11 days ago4 views
👨‍💻Programming
/* =====================================================
   TrainGo — Login & Register (US_UI_001)
   Requires store.js to be loaded first.
   ===================================================== */

// ===== ALREADY SIGNED IN? SKIP THE FORM =====
(() => {
  const s = DB.session.get();
  if (s) {
    window.location.href = s.role === "admin" ? "admin.html" : "home.html";
  }
})();

// ===== DOM =====
const loginCard = document.getElementById("login-card");
const registerCard = document.getElementById("register-card");
const loginForm = document.getElementById("login-form");
const registerForm = document.getElementById("register-form");
const toast = document.getElementById("toast");

// ===== VIEW SWITCHING =====
document.getElementById("go-register").addEventListener("click", () => {
  loginCard.classList.remove("active");
  registerCard.classList.add("active");
  clearForm(loginForm);
});

document.getElementById("go-login").addEventListener("click", () => {
  registerCard.classList.remove("active");
  loginCard.classList.add("active");
  clearForm(registerForm);
});

// ===== PASSWORD TOGGLE =====
document.querySelectorAll(".toggle-pw").forEach((btn) => {
  btn.addEventListener("click", () => {
    const input = document.getElementById(btn.dataset.target);
    const isPassword = input.type === "password";
    input.type = isPassword ? "text" : "password";
    btn.textContent = isPassword ? "🙈" : "👁";
  });
});

// ===== UI HELPERS =====
function showToast(message, isError = false) {
  toast.textContent = message;
  toast.classList.toggle("toast--error", isError);
  toast.classList.add("show");
  setTimeout(() => toast.classList.remove("show"), 3000);
}

function setError(errId, msg) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = msg;
  const field =
    el.previousElementSibling?.querySelector?.("input") ||
    el.previousElementSibling;
  if (field && field.tagName === "INPUT") {
    field.classList.remove("valid");
    field.classList.add("invalid");
  }
}

function setValid(errId) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = "";
  const field =
    el.previousElementSibling?.querySelector?.("input") ||
    el.previousElementSibling;
  if (field && field.tagName === "INPUT") {
    field.classList.remove("invalid");
    field.classList.add("valid");
  }
}

function clearForm(form) {
  form.reset();
  form.querySelectorAll(".error-msg").forEach((e) => (e.textContent = ""));
  form.querySelectorAll("input").forEach((i) =>
    i.classList.remove("invalid", "valid")
  );
}

// ===== VALIDATOR REGISTRY =====
const validators = {};

function attachLiveValidation(inputId, errId, validator) {
  validators[inputId] = validator;
  const input = document.getElementById(inputId);
  if (!input) return;
  input.addEventListener("input", () => {
    const msg = validator(input.value.trim());
    msg ? setError(errId, msg) : setValid(errId);
  });
}

function runValidation(fields) {
  let ok = true;
  fields.forEach(([inputId, errId]) => {
    const input = document.getElementById(inputId);
    const validator = validators[inputId];
    if (!input) { console.warn(`Missing input: #${inputId}`); return; }
    if (!validator) { console.warn(`No validator: #${inputId}`); return; }

    const msg = validator(input.value.trim());
    if (msg) { setError(errId, msg); ok = false; }
    else { setValid(errId); }
  });
  return ok;
}

// ===== LOGIN VALIDATORS =====
attachLiveValidation("login-username", "login-username-err", (v) =>
  v === "" ? "Username is required." : null
);

attachLiveValidation("login-password", "login-password-err", (v) =>
  v === "" ? "Password is required." : null
);

// ===== REGISTER VALIDATORS (per US_UI_001 spec) =====

// 1. username — min 6 chars, no special chars or numbers, not null
attachLiveValidation("reg-username", "reg-username-err", (v) => {
  if (v === "") return "Username is required.";
  if (v.length < 6) return "Must be at least 6 characters.";
  if (!/^[A-Za-z]+$/.test(v)) return "Letters only — no numbers or symbols.";
  return null;
});

// 2. password — min 8, 1 special, 1 number, 1 uppercase, not null
attachLiveValidation("reg-password", "reg-password-err", (v) => {
  if (v === "") return "Password is required.";
  if (v.length < 8) return "Must be at least 8 characters.";
  if (!/[A-Z]/.test(v)) return "Needs at least 1 uppercase letter.";
  if (!/[0-9]/.test(v)) return "Needs at least 1 number.";
  if (!/[^A-Za-z0-9]/.test(v)) return "Needs at least 1 special character.";
  return null;
});

// 3. confirm password — must match, not null
attachLiveValidation("reg-confirm", "reg-confirm-err", (v) => {
  if (v === "") return "Please confirm your password.";
  if (v !== document.getElementById("reg-password").value)
    return "Passwords do not match.";
  return null;
});

// 4. mail — contains @, has a domain part, not null
attachLiveValidation("reg-email", "reg-email-err", (v) => {
  if (v === "") return "Email is required.";
  if (!v.includes("@")) return "Must contain an @ sign.";
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v))
    return "Must include a domain like gmail.com.";
  return null;
});

// 5. mobile — no alphabets, exactly 10 digits, not null
attachLiveValidation("reg-mobile", "reg-mobile-err", (v) => {
  if (v === "") return "Mobile number is required.";
  if (/[A-Za-z]/.test(v)) return "Alphabets are not allowed.";
  if (!/^[0-9]{10}$/.test(v)) return "Must be exactly 10 digits.";
  return null;
});

// 6. aadhaar — not null
attachLiveValidation("reg-aadhaar", "reg-aadhaar-err", (v) => {
  if (v === "") return "Aadhaar number is required.";
  if (!/^[0-9]{12}$/.test(v)) return "Must be 12 digits.";
  return null;
});

// ===== LOGIN SUBMIT =====
loginForm.addEventListener("submit", (e) => {
  e.preventDefault();

  const fields = [
    ["login-username", "login-username-err"],
    ["login-password", "login-password-err"],
  ];

  if (!runValidation(fields)) return;

  const username = document.getElementById("login-username").value.trim();
  const password = document.getElementById("login-password").value;

  // does the account exist?
  if (!DB.users.find(username)) {
    setError("login-username-err", "No account found with this username.");
    return;
  }

  // does the password match?
  const user = DB.users.authenticate(username, password);
  if (!user) {
    setError("login-password-err", "Incorrect password.");
    return;
  }

  // role comes from the stored record, never from the form
  DB.session.set(user.username, user.role);

  const isAdmin = user.role === "admin";
  const destination = isAdmin ? "admin.html" : "home.html";

  const btn = loginForm.querySelector(".btn");
  const btnText = btn.querySelector(".btn__text") || btn;
  btnText.textContent = "Boarding…";
  btn.disabled = true;

  setTimeout(() => {
    showToast(isAdmin ? "🛡️  Admin login successful." : "✓  Welcome aboard!");
    setTimeout(() => (window.location.href = destination), 700);
  }, 1000);
});

// ===== REGISTER SUBMIT =====
registerForm.addEventListener("submit", (e) => {
  e.preventDefault();

  const fields = [
    ["reg-username", "reg-username-err"],
    ["reg-email", "reg-email-err"],
    ["reg-password", "reg-password-err"],
    ["reg-confirm", "reg-confirm-err"],
    ["reg-mobile", "reg-mobile-err"],
    ["reg-aadhaar", "reg-aadhaar-err"],
  ];

  if (!runValidation(fields)) return;

  const username = document.getElementById("reg-username").value.trim();

  if (DB.users.exists(username)) {
    setError("reg-username-err", "Username already taken.");
    return;
  }

  const created = DB.users.add({
    username,
    role: "customer",
    email: document.getElementById("reg-email").value.trim(),
    password: document.getElementById("reg-password").value,
    mobile: document.getElementById("reg-mobile").value.trim(),
    aadhaar: document.getElementById("reg-aadhaar").value.trim(),
  });

  if (!created) {
    showToast("Registration failed — storage error.", true);
    return;
  }

  const btn = registerForm.querySelector(".btn");
  const btnText = btn.querySelector(".btn__text") || btn;
  btnText.textContent = "Issuing ticket…";
  btn.disabled = true;

  setTimeout(() => {
    showToast(`🎫  Account ${created.userId} created! Please log in.`);
    btnText.textContent = "Register";
    btn.disabled = false;
    clearForm(registerForm);

    setTimeout(() => {
      registerCard.classList.remove("active");
      loginCard.classList.add("active");
    }, 1200);
  }, 1000);
});

/* =====================================================
   TrainGo — Customer Portal (US_UI_002)
   Requires store.js to be loaded first.
   ===================================================== */

// ===== ACCESS GUARD =====
(() => {
  const s = DB.session.get();
  if (!s) { window.location.href = "index.html"; return; }
  if (s.role === "admin") { window.location.href = "admin.html"; }
})();

// ===== STATIC DATA =====
const STATIONS = [
  "New Delhi", "Mumbai Central", "Chennai Central", "Howrah Junction",
  "Bengaluru City", "Hyderabad Deccan", "Pune Junction", "Ahmedabad Junction",
  "Jaipur Junction", "Lucknow Charbagh", "Bhopal Junction", "Patna Junction",
];

// ===== DOM =====
const toast = document.getElementById("toast");
const cancelModal = document.getElementById("cancel-modal");
let pendingCancelId = null;

// ===== UI HELPERS =====
function showToast(message, isError = false) {
  toast.textContent = message;
  toast.classList.toggle("toast--error", isError);
  toast.classList.add("show");
  setTimeout(() => toast.classList.remove("show"), 3000);
}

function setError(errId, msg) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = msg;
  const field = el.parentElement.querySelector("input, select, textarea");
  if (field) { field.classList.remove("valid"); field.classList.add("invalid"); }
}

function setValid(errId) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = "";
  const field = el.parentElement.querySelector("input, select, textarea");
  if (field) { field.classList.remove("invalid"); field.classList.add("valid"); }
}

function clearFormState(form) {
  form.querySelectorAll(".error-msg").forEach((e) => (e.textContent = ""));
  form.querySelectorAll("input, select, textarea")
      .forEach((f) => f.classList.remove("invalid", "valid"));
}

function escapeHTML(str) {
  const map = {
    "&": "&amp;", "<": "&lt;", ">": "&gt;",
    '"': "&quot;", "'": "&#39;",
  };
  return String(str).replace(/[&<>"']/g, (c) => map[c]);
}

// ===== VALIDATOR REGISTRY =====
const validators = {};

function attachLiveValidation(inputId, errId, validator) {
  validators[inputId] = validator;
  const input = document.getElementById(inputId);
  if (!input) return;
  const evt = input.tagName === "SELECT" ? "change" : "input";
  input.addEventListener(evt, () => {
    const msg = validator(input.value.trim());
    msg ? setError(errId, msg) : setValid(errId);
  });
}

function runValidation(fields) {
  let ok = true;
  fields.forEach(([inputId, errId]) => {
    const input = document.getElementById(inputId);
    const validator = validators[inputId];
    if (!input || !validator) return;
    const msg = validator(input.value.trim());
    if (msg) { setError(errId, msg); ok = false; }
    else { setValid(errId); }
  });
  return ok;
}

// ===== TAB SWITCHING =====
function showPanel(panelId) {
  document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active"));
  document.getElementById(panelId)?.classList.add("active");
  document.querySelectorAll(".tab").forEach((t) =>
    t.classList.toggle("active", t.dataset.panel === panelId)
  );
  window.scrollTo({ top: 0, behavior: "smooth" });
}

document.querySelectorAll(".tab").forEach((tab) => {
  tab.addEventListener("click", () => {
    const panelId = tab.dataset.panel;
    showPanel(panelId);
    if (panelId === "panel-home") renderAggregates();
    if (panelId === "panel-view") renderTickets();
    if (panelId === "panel-update") loadUpdateForm();
  });
});

// ===== SESSION =====
function initSession() {
  const user = DB.session.user();
  if (!user) return null;

  document.getElementById("current-user").textContent = user.username;
  document.getElementById("bk-id").value = user.userId;

  const nameField = document.getElementById("bk-name");
  if (nameField && !nameField.value) nameField.value = user.username;
  return user;
}

document.getElementById("logout-btn").addEventListener("click", () => {
  DB.session.clear();
  window.location.href = "index.html";
});

// ===== STATION DROPDOWNS =====
function populateStations() {
  const from = document.getElementById("bk-from");
  const to = document.getElementById("bk-to");
  STATIONS.forEach((s) => {
    from.insertAdjacentHTML("beforeend", `<option value="${s}">${s}</option>`);
    to.insertAdjacentHTML("beforeend", `<option value="${s}">${s}</option>`);
  });
}

// ===== 2.7 AVAILABLE TRAINS (from admin-registered trains) =====
function refreshTrains() {
  const from = document.getElementById("bk-from").value;
  const to = document.getElementById("bk-to").value;
  const trainSelect = document.getElementById("bk-train");

  trainSelect.innerHTML = "";

  if (!from || !to) {
    trainSelect.innerHTML = `<option value="">Select boarding & destination first</option>`;
    return;
  }
  if (from === to) {
    trainSelect.innerHTML = `<option value="">Stations must differ</option>`;
    return;
  }

  const matching = DB.trains.onRoute(from, to);

  if (matching.length === 0) {
    trainSelect.innerHTML = `<option value="">No trains registered on this route</option>`;
    return;
  }

  trainSelect.innerHTML = `<option value="">Select a train</option>`;
  matching.forEach((t) => {
    trainSelect.insertAdjacentHTML(
      "beforeend",
      `<option value="${escapeHTML(t.id)}">${escapeHTML(t.id)} — ${escapeHTML(t.name)} (dep ${escapeHTML(t.depart)})</option>`
    );
  });
}

document.getElementById("bk-from").addEventListener("change", refreshTrains);
document.getElementById("bk-to").addEventListener("change", refreshTrains);

// ===== BOOKING VALIDATORS =====
attachLiveValidation("bk-name", "bk-name-err", (v) => {
  if (v === "") return "Passenger name is required.";
  if (!/^[A-Za-z ]+$/.test(v)) return "Letters and spaces only.";
  return null;
});

attachLiveValidation("bk-mobile", "bk-mobile-err", (v) => {
  if (v === "") return "Mobile number is required.";
  if (/[A-Za-z]/.test(v)) return "Alphabets are not allowed.";
  if (!/^[0-9]{10}$/.test(v)) return "Must be exactly 10 digits.";
  return null;
});

attachLiveValidation("bk-age", "bk-age-err", (v) => {
  if (v === "") return "Age is required.";
  if (!/^[0-9]{1,3}$/.test(v)) return "Digits only.";
  const n = Number(v);
  if (n < 1 || n > 120) return "Enter a valid age (1-120).";
  return null;
});

attachLiveValidation("bk-date", "bk-date-err", (v) => {
  if (v === "") return "Journey date is required.";
  const today = new Date(); today.setHours(0, 0, 0, 0);
  if (new Date(v) < today) return "Date cannot be in the past.";
  return null;
});

attachLiveValidation("bk-from", "bk-from-err", (v) =>
  v === "" ? "Boarding station is required." : null
);

attachLiveValidation("bk-to", "bk-to-err", (v) => {
  if (v === "") return "Destination station is required.";
  if (v === document.getElementById("bk-from").value)
    return "Destination must differ from boarding.";
  return null;
});

attachLiveValidation("bk-class", "bk-class-err", (v) =>
  v === "" ? "Ticket category is required." : null
);

attachLiveValidation("bk-train", "bk-train-err", (v) =>
  v === "" ? "Please select a train." : null
);

attachLiveValidation("bk-count", "bk-count-err", (v) => {
  if (v === "") return "Number of tickets is required.";
  if (!/^[0-9]+$/.test(v)) return "Digits only.";
  const n = Number(v);
  if (n < 1 || n > 6) return "Between 1 and 6 tickets.";
  return null;
});

// ===== BOOKING SUBMIT =====
const bookForm = document.getElementById("book-form");

bookForm.addEventListener("submit", (e) => {
  e.preventDefault();

  const fields = [
    ["bk-name", "bk-name-err"],
    ["bk-mobile", "bk-mobile-err"],
    ["bk-age", "bk-age-err"],
    ["bk-date", "bk-date-err"],
    ["bk-from", "bk-from-err"],
    ["bk-to", "bk-to-err"],
    ["bk-class", "bk-class-err"],
    ["bk-train", "bk-train-err"],
    ["bk-count", "bk-count-err"],
  ];

  if (!runValidation(fields)) {
    showToast("Please fix the highlighted fields.", true);
    return;
  }

  const trainId = document.getElementById("bk-train").value;
  const train = DB.trains.find(trainId);

  if (!train) {
    setError("bk-train-err", "That train is no longer available.");
    refreshTrains();
    return;
  }

  const date = document.getElementById("bk-date").value;
  const count = Number(document.getElementById("bk-count").value);

  // capacity check against seats already sold that day
  const taken = DB.tickets.seatsTaken(train.id, date);
  const free = train.seats - taken;

  if (count > free) {
    setError("bk-count-err", free > 0
      ? `Only ${free} seat${free === 1 ? "" : "s"} left on this train.`
      : "This train is fully booked for that date.");
    showToast("Not enough seats available.", true);
    return;
  }

  const boardingDT = `${date} ${train.depart}`;
  const arrivalDT = computeArrival(date, train.depart, train.duration);

  const ticket = DB.tickets.add({
    trainId: train.id,
    trainName: train.name,
    userId: document.getElementById("bk-id").value,
    userName: document.getElementById("bk-name").value.trim(),
    mobile: document.getElementById("bk-mobile").value.trim(),
    age: document.getElementById("bk-age").value.trim(),
    boarding: document.getElementById("bk-from").value,
    destination: document.getElementById("bk-to").value,
    category: document.getElementById("bk-class").value,
    boardingDT,
    arrivalDT,
    count,
  });

  if (!ticket) {
    showToast("Booking failed — storage error.", true);
    return;
  }

  renderConfirmation(ticket);
  bookForm.reset();
  clearFormState(bookForm);
  initSession();
  refreshTrains();
  showPanel("panel-confirm");
});

// arrival = boarding datetime + duration hours
function computeArrival(dateStr, departTime, durationHrs) {
  const [h, m] = String(departTime).split(":").map(Number);
  const dt = new Date(dateStr);
  dt.setHours(isNaN(h) ? 0 : h, isNaN(m) ? 0 : m, 0, 0);
  dt.setMinutes(dt.getMinutes() + Math.round(Number(durationHrs) * 60));

  const pad = (n) => String(n).padStart(2, "0");
  return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ` +
         `${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
}

// ===== CONFIRMATION =====
function renderConfirmation(t) {
  const rows = [
    ["Ticket ID", t.ticketId],
    ["Train ID", `${t.trainId} — ${t.trainName}`],
    ["User ID", t.userId],
    ["Passenger", t.userName],
    ["Boarding Station", t.boarding],
    ["Destination Station", t.destination],
    ["Boarding Date & Time", t.boardingDT],
    ["Arrival Date & Time", t.arrivalDT],
    ["Category", t.category],
    ["Number of Tickets", t.count],
  ];

  document.getElementById("confirm-list").innerHTML = rows
    .map(([k, v]) =>
      `<div class="confirm-item"><dt>${k}</dt><dd>${escapeHTML(String(v))}</dd></div>`)
    .join("");
}

document.getElementById("confirm-view").addEventListener("click", () => {
  renderTickets();
  showPanel("panel-view");
});

document.getElementById("confirm-another").addEventListener("click", () => {
  showPanel("panel-book");
});

// ===== 3. VIEW TICKETS =====
function renderTickets() {
  const body = document.getElementById("tickets-body");
  const user = DB.session.user();
  if (!user) return;

  // only this customer's tickets
  const tickets = DB.tickets.byUser(user.userId)
    .concat(DB.tickets.byUser(user.username))
    .filter((t, i, arr) => arr.findIndex((x) => x.ticketId === t.ticketId) === i);

  if (tickets.length === 0) {
    body.innerHTML = `<tr><td colspan="11" class="empty-row">No tickets booked yet.</td></tr>`;
    return;
  }

  body.innerHTML = tickets
    .slice()
    .reverse()
    .map((t) => {
      const cancelled = t.status === "Cancelled";
      return `
      <tr>
        <td><strong>${escapeHTML(t.ticketId)}</strong></td>
        <td>${escapeHTML(t.trainId)}</td>
        <td>${escapeHTML(t.userId)}</td>
        <td>${escapeHTML(t.userName)}</td>
        <td>${escapeHTML(t.boarding)}</td>
        <td>${escapeHTML(t.destination)}</td>
        <td>${escapeHTML(t.boardingDT)}</td>
        <td>${escapeHTML(t.arrivalDT)}</td>
        <td>${t.count}</td>
        <td><span class="pill ${cancelled ? "pill--cancelled" : "pill--active"}">${t.status}</span></td>
        <td>
          <button class="btn-cancel" data-ticket="${escapeHTML(t.ticketId)}" ${cancelled ? "disabled" : ""}>
            ${cancelled ? "Cancelled" : "Cancel"}
          </button>
        </td>
      </tr>`;
    })
    .join("");

  body.querySelectorAll(".btn-cancel:not([disabled])").forEach((btn) => {
    btn.addEventListener("click", () => openCancelModal(btn.dataset.ticket));
  });
}

// ===== 3.10.1 CANCEL CONFIRMATION =====
function openCancelModal(ticketId) {
  pendingCancelId = ticketId;
  document.getElementById("cancel-ticket-id").textContent = ticketId;
  cancelModal.classList.add("is-open");
}

function closeCancelModal() {
  pendingCancelId = null;
  cancelModal.classList.remove("is-open");
}

document.getElementById("cancel-no").addEventListener("click", closeCancelModal);

cancelModal.addEventListener("click", (e) => {
  if (e.target === cancelModal) closeCancelModal();
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && cancelModal.classList.contains("is-open")) {
    closeCancelModal();
  }
});

document.getElementById("cancel-yes").addEventListener("click", () => {
  if (!pendingCancelId) return;

  const ok = DB.tickets.cancel(pendingCancelId);
  closeCancelModal();
  renderTickets();
  renderAggregates();
  showToast(ok ? "Ticket cancelled." : "Cancellation failed.", !ok);
});

// ===== 1. TABLE AGGREGATES =====
function renderAggregates() {
  const user = DB.session.user();
  if (!user) return;

  const mine = DB.tickets.byUser(user.userId)
    .concat(DB.tickets.byUser(user.username))
    .filter((t, i, arr) => arr.findIndex((x) => x.ticketId === t.ticketId) === i);

  const active = mine.filter((t) => t.status === "Active");

  document.getElementById("stat-total").textContent = mine.length;
  document.getElementById("stat-active").textContent = active.length;
  document.getElementById("stat-cancelled").textContent = mine.length - active.length;

  // 1.1 per class
  const byClass = {};
  active.forEach((t) => { byClass[t.category] = (byClass[t.category] || 0) + t.count; });
  const totalSeats = Object.values(byClass).reduce((a, b) => a + b, 0);
  const classBody = document.getElementById("class-agg-body");

  classBody.innerHTML = totalSeats === 0
    ? `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`
    : Object.entries(byClass)
        .sort((a, b) => b[1] - a[1])
        .map(([cat, n]) => {
          const pct = Math.round((n / totalSeats) * 100);
          return `<tr>
            <td>${escapeHTML(cat)}</td>
            <td>${n}</td>
            <td><div class="bar-cell">
              <div class="bar" style="width:${Math.max(pct * 1.4, 4)}px"></div>
              <span>${pct}%</span>
            </div></td>
          </tr>`;
        }).join("");

  // 1.2 per quarter
  const byQ = {};
  active.forEach((t) => {
    const d = new Date(t.bookedOn);
    const q = `Q${Math.floor(d.getMonth() / 3) + 1} ${d.getFullYear()}`;
    if (!byQ[q]) byQ[q] = { bookings: 0, seats: 0 };
    byQ[q].bookings += 1;
    byQ[q].seats += t.count;
  });

  const qKeys = Object.keys(byQ).sort();
  const qBody = document.getElementById("quarter-agg-body");

  qBody.innerHTML = qKeys.length === 0
    ? `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`
    : qKeys.map((q) =>
        `<tr><td>${q}</td><td>${byQ[q].bookings}</td><td>${byQ[q].seats}</td></tr>`
      ).join("");
}

// ===== 4. UPDATE DETAILS =====
const updateForm = document.getElementById("update-form");
const pwToggle = document.getElementById("up-toggle-pw");
const pwSection = document.getElementById("pw-section");

pwToggle.addEventListener("change", () => {
  pwSection.classList.toggle("is-open", pwToggle.checked);
  if (!pwToggle.checked) {
    ["up-current", "up-new", "up-confirm"].forEach((id) => {
      const el = document.getElementById(id);
      el.value = "";
      el.classList.remove("invalid", "valid");
      document.getElementById(id + "-err").textContent = "";
    });
  }
});

function loadUpdateForm() {
  const user = DB.session.user();
  if (!user) return;
  document.getElementById("up-email").value = user.email || "";
  document.getElementById("up-mobile").value = user.mobile || "";
  document.getElementById("up-address").value = user.address || "";
}

// same rules as registration
attachLiveValidation("up-email", "up-email-err", (v) => {
  if (v === "") return "Email is required.";
  if (!v.includes("@")) return "Must contain an @ sign.";
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return "Must include a domain like gmail.com.";
  return null;
});

attachLiveValidation("up-mobile", "up-mobile-err", (v) => {
  if (v === "") return "Mobile number is required.";
  if (/[A-Za-z]/.test(v)) return "Alphabets are not allowed.";
  if (!/^[0-9]{10}$/.test(v)) return "Must be exactly 10 digits.";
  return null;
});

attachLiveValidation("up-address", "up-address-err", (v) =>
  v === "" ? "Address is required." : null
);

attachLiveValidation("up-current", "up-current-err", (v) =>
  v === "" ? "Current password is required." : null
);

attachLiveValidation("up-new", "up-new-err", (v) => {
  if (v === "") return "New password is required.";
  if (v.length < 8) return "Must be at least 8 characters.";
  if (!/[A-Z]/.test(v)) return "Needs at least 1 uppercase letter.";
  if (!/[0-9]/.test(v)) return "Needs at least 1 number.";
  if (!/[^A-Za-z0-9]/.test(v)) return "Needs at least 1 special character.";
  return null;
});

attachLiveValidation("up-confirm", "up-confirm-err", (v) => {
  if (v === "") return "Please confirm the new password.";
  if (v !== document.getElementById("up-new").value) return "Passwords do not match.";
  return null;
});

updateForm.addEventListener("submit", (e) => {
  e.preventDefault();

  let fields = [
    ["up-email", "up-email-err"],
    ["up-mobile", "up-mobile-err"],
    ["up-address", "up-address-err"],
  ];

  if (pwToggle.checked) {
    fields = fields.concat([
      ["up-current", "up-current-err"],
      ["up-new", "up-new-err"],
      ["up-confirm", "up-confirm-err"],
    ]);
  }

  if (!runValidation(fields)) {
    showToast("Please fix the highlighted fields.", true);
    return;
  }

  const user = DB.session.user();
  if (!user) {
    showToast("Session expired. Please log in again.", true);
    setTimeout(() => (window.location.href = "index.html"), 1200);
    return;
  }

  const patch = {
    email: document.getElementById("up-email").value.trim(),
    mobile: document.getElementById("up-mobile").value.trim(),
    address: document.getElementById("up-address").value.trim(),
  };

  // 4.4 password change — verify current first
  if (pwToggle.checked) {
    if (user.password !== document.getElementById("up-current").value) {
      setError("up-current-err", "Current password is incorrect.");
      showToast("Current password is incorrect.", true);
      return;
    }
    patch.password = document.getElementById("up-new").value;
  }

  const updated = DB.users.update(user.username, patch);

  if (!updated) {
    showToast("Update failed — storage error.", true);
    return;
  }

  pwToggle.checked = false;
  pwSection.classList.remove("is-open");
  ["up-current", "up-new", "up-confirm"].forEach((id) => {
    document.getElementById(id).value = "";
  });
  clearFormState(updateForm);

  showToast("✓  Details updated successfully.");
});

// ===== INIT =====
populateStations();
initSession();
loadUpdateForm();
renderAggregates();
renderTickets();
refreshTrains();

// journey date cannot be before tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById("bk-date").min = tomorrow.toISOString().split("T")[0];

/* =====================================================
   TrainGo — Admin Portal (US_UI_003)
   Requires store.js to be loaded first.
   ===================================================== */

const STATIONS = [
  "New Delhi", "Mumbai Central", "Chennai Central", "Howrah Junction",
  "Bengaluru City", "Hyderabad Deccan", "Pune Junction", "Ahmedabad Junction",
  "Jaipur Junction", "Lucknow Charbagh", "Bhopal Junction", "Patna Junction",
];

const toast = document.getElementById("toast");
const confirmModal = document.getElementById("confirm-modal");
let pendingAction = null;

/* ===== UI HELPERS ===== */
function showToast(msg, isError = false) {
  toast.textContent = msg;
  toast.classList.toggle("toast--error", isError);
  toast.classList.add("show");
  setTimeout(() => toast.classList.remove("show"), 3000);
}

function setError(errId, msg) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = msg;
  const f = el.parentElement.querySelector("input, select, textarea");
  if (f) { f.classList.remove("valid"); f.classList.add("invalid"); }
}

function setValid(errId) {
  const el = document.getElementById(errId);
  if (!el) return;
  el.textContent = "";
  const f = el.parentElement.querySelector("input, select, textarea");
  if (f) { f.classList.remove("invalid"); f.classList.add("valid"); }
}

function clearFormState(form) {
  form.querySelectorAll(".error-msg").forEach((e) => (e.textContent = ""));
  form.querySelectorAll("input, select, textarea")
      .forEach((f) => f.classList.remove("invalid", "valid"));
}

function escapeHTML(str) {
  const map = {
    "&": "&amp;", "<": "&lt;", ">": "&gt;",
    '"': "&quot;", "'": "&#39;",
  };
  return String(str).replace(/[&<>"']/g, (c) => map[c]);
}

/* ===== VALIDATORS ===== */
const validators = {};

function attachLiveValidation(inputId, errId, fn) {
  validators[inputId] = fn;
  const input = document.getElementById(inputId);
  if (!input) return;
  const evt = input.tagName === "SELECT" ? "change" : "input";
  input.addEventListener(evt, () => {
    const msg = fn(input.value.trim());
    msg ? setError(errId, msg) : setValid(errId);
  });
}

function runValidation(fields) {
  let ok = true;
  fields.forEach(([inputId, errId]) => {
    const input = document.getElementById(inputId);
    const fn = validators[inputId];
    if (!input || !fn) return;
    const msg = fn(input.value.trim());
    if (msg) { setError(errId, msg); ok = false; }
    else { setValid(errId); }
  });
  return ok;
}

/* ===== TABS ===== */
function showPanel(id) {
  document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active"));
  document.getElementById(id)?.classList.add("active");
  document.querySelectorAll(".tab").forEach((t) =>
    t.classList.toggle("active", t.dataset.panel === id)
  );
  window.scrollTo({ top: 0, behavior: "smooth" });
}

document.querySelectorAll(".tab").forEach((tab) => {
  tab.addEventListener("click", () => {
    const id = tab.dataset.panel;
    showPanel(id);
    if (id === "panel-home") renderDashboard();
    if (id === "panel-train") renderTrains();
    if (id === "panel-profile") loadProfile();
  });
});

/* ===== ACCESS GUARD ===== */
function guardAdmin() {
  const s = DB.session.get();

  if (!s) { window.location.href = "index.html"; return false; }
  if (s.role !== "admin") { window.location.href = "home.html"; return false; }

  document.getElementById("current-user").textContent = s.username;
  return true;
}

document.getElementById("logout-btn").addEventListener("click", () => {
  DB.session.clear();
  window.location.href = "index.html";
});

/* ===== CONFIRM MODAL ===== */
function openConfirm(title, text, onYes) {
  document.getElementById("confirm-title").textContent = title;
  document.getElementById("confirm-text").innerHTML = text;
  pendingAction = onYes;
  confirmModal.classList.add("is-open");
}

function closeConfirm() {
  pendingAction = null;
  confirmModal.classList.remove("is-open");
}

document.getElementById("confirm-no").addEventListener("click", closeConfirm);

document.getElementById("confirm-yes").addEventListener("click", () => {
  const fn = pendingAction;
  closeConfirm();
  if (typeof fn === "function") fn();
});

confirmModal.addEventListener("click", (e) => {
  if (e.target === confirmModal) closeConfirm();
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && confirmModal.classList.contains("is-open")) closeConfirm();
});

/* ===== 1 + 2. DASHBOARD ===== */
function renderDashboard() {
  const customers = DB.users.customers();
  const active = DB.tickets.active();

  document.getElementById("stat-customers").textContent = customers.length;
  document.getElementById("stat-active").textContent = active.length;
  document.getElementById("stat-trains").textContent = DB.trains.all().length;

  /* 1.1 tickets per class — system wide */
  const byClass = DB.tickets.countByClass();
  const totalSeats = Object.values(byClass).reduce((a, b) => a + b, 0);
  const classBody = document.getElementById("class-agg-body");

  classBody.innerHTML = totalSeats === 0
    ? `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`
    : Object.entries(byClass)
        .sort((a, b) => b[1] - a[1])
        .map(([cat, n]) => {
          const pct = Math.round((n / totalSeats) * 100);
          return `<tr>
            <td>${escapeHTML(cat)}</td>
            <td>${n}</td>
            <td><div class="bar-cell">
              <div class="bar" style="width:${Math.max(pct * 1.4, 4)}px"></div>
              <span>${pct}%</span>
            </div></td>
          </tr>`;
        }).join("");

  /* 1.2 sales per quarter */
  const byQ = DB.tickets.countByQuarter();
  const qKeys = Object.keys(byQ).sort();
  const qBody = document.getElementById("quarter-agg-body");

  qBody.innerHTML = qKeys.length === 0
    ? `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`
    : qKeys.map((q) =>
        `<tr><td>${q}</td><td>${byQ[q].bookings}</td><td>${byQ[q].seats}</td></tr>`
      ).join("");

  /* 2. customer records */
  const custBody = document.getElementById("customers-body");

  custBody.innerHTML = customers.length === 0
    ? `<tr><td colspan="5" class="empty-row">No customers registered yet.</td></tr>`
    : customers.map((u) => {
        const booked = active
          .filter((t) => t.userId === u.userId || t.userName === u.username)
          .reduce((sum, t) => sum + t.count, 0);
        return `<tr>
          <td><strong>${escapeHTML(u.userId || "—")}</strong></td>
          <td>${escapeHTML(u.username)}</td>
          <td>${escapeHTML(u.mobile || "—")}</td>
          <td>${booked}</td>
          <td><button class="btn-delete" data-user="${escapeHTML(u.username)}">Delete</button></td>
        </tr>`;
      }).join("");

  custBody.querySelectorAll(".btn-delete").forEach((btn) => {
    btn.addEventListener("click", () => {
      const uname = btn.dataset.user;
      openConfirm(
        "Delete Customer?",
        `This will permanently remove <strong>${escapeHTML(uname)}</strong> and all their tickets.`,
        () => {
          DB.users.remove(uname);
          renderDashboard();
          showToast(`Customer "${uname}" deleted.`);
        }
      );
    });
  });
}

/* ===== REGISTER A TRAIN ===== */
function populateStations() {
  const from = document.getElementById("tr-from");
  const to = document.getElementById("tr-to");
  STATIONS.forEach((s) => {
    from.insertAdjacentHTML("beforeend", `<option value="${s}">${s}</option>`);
    to.insertAdjacentHTML("beforeend", `<option value="${s}">${s}</option>`);
  });
}

attachLiveValidation("tr-name", "tr-name-err", (v) => {
  if (v === "") return "Train name is required.";
  if (v.length < 3) return "Must be at least 3 characters.";
  return null;
});

attachLiveValidation("tr-seats", "tr-seats-err", (v) => {
  if (v === "") return "Number of seats is required.";
  if (!/^[0-9]+$/.test(v)) return "Digits only.";
  const n = Number(v);
  if (n < 1 || n > 2000) return "Enter between 1 and 2000.";
  return null;
});

attachLiveValidation("tr-from", "tr-from-err", (v) =>
  v === "" ? "Origin station is required." : null
);

attachLiveValidation("tr-to", "tr-to-err", (v) => {
  if (v === "") return "Destination station is required.";
  if (v === document.getElementById("tr-from").value)
    return "Destination must differ from origin.";
  return null;
});

attachLiveValidation("tr-depart", "tr-depart-err", (v) =>
  v === "" ? "Departure time is required." : null
);

attachLiveValidation("tr-duration", "tr-duration-err", (v) => {
  if (v === "") return "Duration is required.";
  if (!/^[0-9]+(\.[0-9]+)?$/.test(v)) return "Numbers only (e.g. 16.5).";
  const n = Number(v);
  if (n <= 0 || n > 72) return "Enter between 0 and 72 hours.";
  return null;
});

attachLiveValidation("tr-owner", "tr-owner-err", (v) =>
  v === "" ? "Ownership is required." : null
);

const trainForm = document.getElementById("train-form");

trainForm.addEventListener("submit", (e) => {
  e.preventDefault();

  const fields = [
    ["tr-name", "tr-name-err"],
    ["tr-seats", "tr-seats-err"],
    ["tr-from", "tr-from-err"],
    ["tr-to", "tr-to-err"],
    ["tr-depart", "tr-depart-err"],
    ["tr-duration", "tr-duration-err"],
    ["tr-owner", "tr-owner-err"],
  ];

  if (!runValidation(fields)) {
    showToast("Please fix the highlighted fields.", true);
    return;
  }

  const train = DB.trains.add({
    name: document.getElementById("tr-name").value.trim(),
    seats: Number(document.getElementById("tr-seats").value),
    from: document.getElementById("tr-from").value,
    to: document.getElementById("tr-to").value,
    depart: document.getElementById("tr-depart").value,
    duration: Number(document.getElementById("tr-duration").value),
    owner: document.getElementById("tr-owner").value,
  });

  if (!train) {
    showToast("Registration failed — storage error.", true);
    return;
  }

  trainForm.reset();
  clearFormState(trainForm);
  renderTrains();
  renderDashboard();
  showToast(`✓  Train ${train.id} registered.`);
});

function renderTrains() {
  const body = document.getElementById("trains-body");
  const trains = DB.trains.all();

  body.innerHTML = trains.length === 0
    ? `<tr><td colspan="8" class="empty-row">No trains registered yet.</td></tr>`
    : trains.slice().reverse().map((t) => `
        <tr>
          <td><strong>${escapeHTML(t.id)}</strong></td>
          <td>${escapeHTML(t.name)}</td>
          <td>${t.seats}</td>
          <td>${escapeHTML(t.from)}</td>
          <td>${escapeHTML(t.to)}</td>
          <td>${escapeHTML(t.depart)}</td>
          <td>${escapeHTML(t.owner)}</td>
          <td><button class="btn-delete" data-train="${escapeHTML(t.id)}">Remove</button></td>
        </tr>`).join("");

  body.querySelectorAll(".btn-delete").forEach((btn) => {
    btn.addEventListener("click", () => {
      const id = btn.dataset.train;
      openConfirm(
        "Remove Train?",
        `Train <strong>${escapeHTML(id)}</strong> will no longer be bookable.`,
        () => {
          DB.trains.remove(id);
          renderTrains();
          renderDashboard();
          showToast(`Train ${id} removed.`);
        }
      );
    });
  });
}

/* ===== PROFILE ===== */
const profileForm = document.getElementById("profile-form");
const pfToggle = document.getElementById("pf-toggle-pw");
const pfSection = document.getElementById("pf-pw-section");

pfToggle.addEventListener("change", () => {
  pfSection.classList.toggle("is-open", pfToggle.checked);
  if (!pfToggle.checked) {
    ["pf-current", "pf-new", "pf-confirm"].forEach((id) => {
      const el = document.getElementById(id);
      el.value = "";
      el.classList.remove("invalid", "valid");
      document.getElementById(id + "-err").textContent = "";
    });
  }
});

function loadProfile() {
  const admin = DB.session.user();
  if (!admin) return;
  document.getElementById("pf-username").value = admin.username || "";
  document.getElementById("pf-email").value = admin.email || "";
  document.getElementById("pf-mobile").value = admin.mobile || "";
}

attachLiveValidation("pf-username", "pf-username-err", (v) => {
  if (v === "") return "Username is required.";
  if (v.length < 6) return "Must be at least 6 characters.";
  if (!/^[A-Za-z]+$/.test(v)) return "Letters only — no numbers or symbols.";
  return null;
});

attachLiveValidation("pf-email", "pf-email-err", (v) => {
  if (v === "") return "Email is required.";
  if (!v.includes("@")) return "Must contain an @ sign.";
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return "Must include a domain like gmail.com.";
  return null;
});

attachLiveValidation("pf-mobile", "pf-mobile-err", (v) => {
  if (v === "") return "Mobile number is required.";
  if (/[A-Za-z]/.test(v)) return "Alphabets are not allowed.";
  if (!/^[0-9]{10}$/.test(v)) return "Must be exactly 10 digits.";
  return null;
});

attachLiveValidation("pf-current", "pf-current-err", (v) =>
  v === "" ? "Current password is required." : null
);

attachLiveValidation("pf-new", "pf-new-err", (v) => {
  if (v === "") return "New password is required.";
  if (v.length < 8) return "Must be at least 8 characters.";
  if (!/[A-Z]/.test(v)) return "Needs at least 1 uppercase letter.";
  if (!/[0-9]/.test(v)) return "Needs at least 1 number.";
  if (!/[^A-Za-z0-9]/.test(v)) return "Needs at least 1 special character.";
  return null;
});

attachLiveValidation("pf-confirm", "pf-confirm-err", (v) => {
  if (v === "") return "Please confirm the new password.";
  if (v !== document.getElementById("pf-new").value) return "Passwords do not match.";
  return null;
});

profileForm.addEventListener("submit", (e) => {
  e.preventDefault();

  let fields = [
    ["pf-username", "pf-username-err"],
    ["pf-email", "pf-email-err"],
    ["pf-mobile", "pf-mobile-err"],
  ];

  if (pfToggle.checked) {
    fields = fields.concat([
      ["pf-current", "pf-current-err"],
      ["pf-new", "pf-new-err"],
      ["pf-confirm", "pf-confirm-err"],
    ]);
  }

  if (!runValidation(fields)) {
    showToast("Please fix the highlighted fields.", true);
    return;
  }

  const admin = DB.session.user();
  if (!admin) {
    showToast("Session expired. Please log in again.", true);
    setTimeout(() => (window.location.href = "index.html"), 1200);
    return;
  }

  const newUsername = document.getElementById("pf-username").value.trim();

  if (newUsername.toLowerCase() !== admin.username.toLowerCase() &&
      DB.users.exists(newUsername)) {
    setError("pf-username-err", "Username already taken.");
    return;
  }

  const patch = {
    username: newUsername,
    email: document.getElementById("pf-email").value.trim(),
    mobile: document.getElementById("pf-mobile").value.trim(),
  };

  if (pfToggle.checked) {
    if (admin.password !== document.getElementById("pf-current").value) {
      setError("pf-current-err", "Current password is incorrect.");
      showToast("Current password is incorrect.", true);
      return;
    }
    patch.password = document.getElementById("pf-new").value;
  }

  // DB.users.update keeps the session and ticket ownership in sync
  const updated = DB.users.update(admin.username, patch);

  if (!updated) {
    showToast("Update failed — username may be taken.", true);
    return;
  }

  document.getElementById("current-user").textContent = updated.username;

  pfToggle.checked = false;
  pfSection.classList.remove("is-open");
  ["pf-current", "pf-new", "pf-confirm"].forEach((id) => {
    document.getElementById(id).value = "";
  });
  clearFormState(profileForm);

  showToast("✓  Profile updated successfully.");
});

/* delete own account */
document.getElementById("delete-self-btn").addEventListener("click", () => {
  const s = DB.session.get();
  if (!s) return;

  openConfirm(
    "Delete Your Account?",
    `Account <strong>${escapeHTML(s.username)}</strong> will be permanently removed and you will be logged out.`,
    () => {
      DB.users.remove(s.username);
      DB.session.clear();
      window.location.href = "index.html";
    }
  );
});

/* ===== INIT ===== */
if (guardAdmin()) {
  populateStations();
  loadProfile();
  renderDashboard();
  renderTrains();
}
← Back to timeline