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
13 days ago5 views
💻Technology
/* =====================================================
   TrainGo — Customer Portal (US_UI_002)
   ===================================================== */

// ===== STORAGE KEYS =====
const USERS_KEY = "traingo_users";
const TICKETS_KEY = "traingo_tickets";
const SESSION_KEY = "traingo_session";

// ===== 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",
];

const TRAINS = [
  { id: "TR-12951", name: "Rajdhani Express",   depart: "16:55", duration: 15.5 },
  { id: "TR-12009", name: "Shatabdi Express",   depart: "06:00", duration: 7.0 },
  { id: "TR-12621", name: "Tamil Nadu Express", depart: "22:30", duration: 20.0 },
  { id: "TR-12301", name: "Howrah Rajdhani",    depart: "16:able", duration: 17.0 },
  { id: "TR-12627", name: "Karnataka Express",  depart: "20:15", duration: 22.5 },
  { id: "TR-12723", name: "Telangana Express",  depart: "18:40", duration: 19.0 },
];

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

// ===== STORAGE HELPERS =====
function readJSON(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}

function writeJSON(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch {
    showToast("Could not save — storage unavailable.", true);
    return false;
  }
}

const getUsers   = () => readJSON(USERS_KEY, []);
const getTickets = () => readJSON(TICKETS_KEY, []);
const saveTickets = (t) => writeJSON(TICKETS_KEY, t);

function getSession() {
  return readJSON(SESSION_KEY, null);
}

function currentUser() {
  const session = getSession();
  if (!session) return null;
  return getUsers().find((u) => u.username === session.username) || 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"));
}

// ===== 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"));
  const target = document.getElementById(panelId);
  if (target) target.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 BOOTSTRAP =====
function initSession() {
  const user = currentUser();

  if (!user) {
    // No session — fall back to a demo user so the page is usable standalone.
    document.getElementById("current-user").textContent = "guest";
    document.getElementById("bk-id").value = "USR-GUEST";
    return null;
  }

  document.getElementById("current-user").textContent = user.username;
  document.getElementById("bk-id").value = user.userId || "USR-" + user.username.toUpperCase().slice(0, 6);
  const nameField = document.getElementById("bk-name");
  if (nameField && !nameField.value) nameField.value = user.username;
  return user;
}

document.getElementById("logout-btn").addEventListener("click", () => {
  localStorage.removeItem(SESSION_KEY);
  showToast("Logged out. Redirecting…");
  setTimeout(() => (window.location.href = "index.html"), 900);
});

// ===== POPULATE 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 =====
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;
  }

  trainSelect.innerHTML = `<option value="">Select a train</option>`;
  TRAINS.forEach((t) => {
    trainSelect.insertAdjacentHTML(
      "beforeend",
      `<option value="${t.id}">${t.id} — ${t.name} (dep ${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 = TRAINS.find((t) => t.id === trainId);
  const date = document.getElementById("bk-date").value;

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

  const ticket = {
    ticketId: "TKT-" + Date.now().toString().slice(-8),
    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: Number(document.getElementById("bk-count").value),
    status: "Active",
    bookedOn: new Date().toISOString(),
  };

  const tickets = getTickets();
  tickets.push(ticket);
  if (!saveTickets(tickets)) 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] = 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(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 PAGE =====
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 tickets = getTickets();

  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.hidden = false;
}

function closeCancelModal() {
  pendingCancelId = null;
  cancelModal.hidden = true;
}

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.hidden) closeCancelModal();
});

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

  const tickets = getTickets();
  const t = tickets.find((x) => x.ticketId === pendingCancelId);
  if (t) {
    t.status = "Cancelled";
    t.cancelledOn = new Date().toISOString();
    saveTickets(tickets);
  }

  closeCancelModal();
  renderTickets();
  renderAggregates();
  showToast("Ticket cancelled.");
});

// ===== 1. TABLE AGGREGATES =====
function renderAggregates() {
  const tickets = getTickets();
  const active = tickets.filter((t) => t.status === "Active");

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

  // 1.1 per class
  const classBody = document.getElementById("class-agg-body");
  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);

  if (totalSeats === 0) {
    classBody.innerHTML = `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`;
  } else {
    classBody.innerHTML = 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 qBody = document.getElementById("quarter-agg-body");
  const byQuarter = {};

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

  const qKeys = Object.keys(byQuarter).sort();

  if (qKeys.length === 0) {
    qBody.innerHTML = `<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>`;
  } else {
    qBody.innerHTML = qKeys
      .map((q) => `
        <tr>
          <td>${q}</td>
          <td>${byQuarter[q].bookings}</td>
          <td>${byQuarter[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.hidden = !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 = currentUser();
  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 users = getUsers();
  const session = getSession();
  const idx = users.findIndex((u) => u.username === (session && session.username));

  if (idx === -1) {
    showToast("No logged-in account found. Please log in again.", true);
    return;
  }

  if (pwToggle.checked) {
    if (users[idx].password !== document.getElementById("up-current").value) {
      setError("up-current-err", "Current password is incorrect.");
      showToast("Current password is incorrect.", true);
      return;
    }
    users[idx].password = document.getElementById("up-new").value;
  }

  users[idx].email = document.getElementById("up-email").value.trim();
  users[idx].mobile = document.getElementById("up-mobile").value.trim();
  users[idx].address = document.getElementById("up-address").value.trim();

  if (!writeJSON(USERS_KEY, users)) return;

  pwToggle.checked = false;
  pwSection.hidden = true;
  ["up-current", "up-new", "up-confirm"].forEach((id) => {
    document.getElementById(id).value = "";
  });
  clearForm
← Back to timeline