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 ago8 views
👨‍💻Programming
/* =====================================================
   TrainGo — Shared Storage Layer (store.js)

   Load this BEFORE script.js / home.js / admin.js:
     <script src="store.js"></script>
     <script src="home.js"></script>

   Everything that touches localStorage goes through here.
   When you swap to a real backend later, only this file
   changes — the pages keep calling the same functions.
   ===================================================== */

const DB = (() => {

  /* ===== KEYS ===== */
  const KEYS = {
    USERS:   "traingo_users",
    TICKETS: "traingo_tickets",
    TRAINS:  "traingo_trains",
    SESSION: "traingo_session",
    COUNTERS:"traingo_counters",
  };

  /* ===== LOW-LEVEL READ / WRITE ===== */
  function read(key, fallback) {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? fallback : JSON.parse(raw);
    } catch (err) {
      console.warn(`Storage read failed for "${key}":`, err);
      return fallback;
    }
  }

  function write(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (err) {
      // QuotaExceededError, or private-mode restrictions
      console.error(`Storage write failed for "${key}":`, err);
      return false;
    }
  }

  function remove(key) {
    try {
      localStorage.removeItem(key);
      return true;
    } catch {
      return false;
    }
  }

  /** True if localStorage is actually usable in this browser/context. */
  function isAvailable() {
    try {
      const probe = "__traingo_probe__";
      localStorage.setItem(probe, "1");
      localStorage.removeItem(probe);
      return true;
    } catch {
      return false;
    }
  }

  /* ===== ID GENERATION =====
     Sequential per-prefix counters, so IDs read cleanly
     (USR-000001) instead of relying on timestamps.     */
  function nextId(prefix, width = 6) {
    const counters = read(KEYS.COUNTERS, {});
    const next = (counters[prefix] || 0) + 1;
    counters[prefix] = next;
    write(KEYS.COUNTERS, counters);
    return `${prefix}-${String(next).padStart(width, "0")}`;
  }

  /* ===== USERS ===== */
  const users = {
    all() {
      return read(KEYS.USERS, []);
    },

    customers() {
      return users.all().filter((u) => u.role !== "admin");
    },

    admins() {
      return users.all().filter((u) => u.role === "admin");
    },

    find(username) {
      if (!username) return null;
      return users.all().find(
        (u) => u.username.toLowerCase() === String(username).toLowerCase()
      ) || null;
    },

    findById(userId) {
      return users.all().find((u) => u.userId === userId) || null;
    },

    exists(username) {
      return users.find(username) !== null;
    },

    /** Adds a user. Returns the created record, or null if the name is taken. */
    add(data) {
      if (users.exists(data.username)) return null;

      const record = {
        userId: data.userId || nextId(data.role === "admin" ? "ADM" : "USR"),
        username: data.username,
        role: data.role || "customer",
        email: data.email || "",
        password: data.password || "",
        mobile: data.mobile || "",
        aadhaar: data.aadhaar || "",
        address: data.address || "",
        createdOn: new Date().toISOString(),
      };

      const list = users.all();
      list.push(record);
      return write(KEYS.USERS, list) ? record : null;
    },

    /** Patches fields on one user. Handles username changes safely. */
    update(username, patch) {
      const list = users.all();
      const idx = list.findIndex((u) => u.username === username);
      if (idx === -1) return null;

      if (patch.username && patch.username !== username && users.exists(patch.username)) {
        return null; // new name collides
      }

      list[idx] = { ...list[idx], ...patch, updatedOn: new Date().toISOString() };
      if (!write(KEYS.USERS, list)) return null;

      // keep tickets and session in sync with a renamed account
      if (patch.username && patch.username !== username) {
        tickets.renameOwner(username, patch.username);
        const s = session.get();
        if (s && s.username === username) {
          session.set(patch.username, list[idx].role);
        }
      }
      return list[idx];
    },

    /** Removes a user and every ticket they own. */
    remove(username) {
      const list = users.all().filter((u) => u.username !== username);
      const ok = write(KEYS.USERS, list);
      tickets.removeByOwner(username);
      return ok;
    },

    /** Returns the user if the password matches, else null. */
    authenticate(username, password) {
      const u = users.find(username);
      if (!u) return null;
      return u.password === password ? u : null;
    },
  };

  /* ===== TICKETS ===== */
  const tickets = {
    all() {
      return read(KEYS.TICKETS, []);
    },

    active() {
      return tickets.all().filter((t) => t.status === "Active");
    },

    cancelled() {
      return tickets.all().filter((t) => t.status === "Cancelled");
    },

    /** Tickets belonging to one user (matches by id or username). */
    byUser(userIdOrName) {
      return tickets.all().filter(
        (t) => t.userId === userIdOrName || t.userName === userIdOrName
      );
    },

    find(ticketId) {
      return tickets.all().find((t) => t.ticketId === ticketId) || null;
    },

    add(data) {
      const record = {
        ticketId: nextId("TKT", 8),
        status: "Active",
        bookedOn: new Date().toISOString(),
        ...data,
      };
      const list = tickets.all();
      list.push(record);
      return write(KEYS.TICKETS, list) ? record : null;
    },

    cancel(ticketId) {
      const list = tickets.all();
      const t = list.find((x) => x.ticketId === ticketId);
      if (!t) return false;
      t.status = "Cancelled";
      t.cancelledOn = new Date().toISOString();
      return write(KEYS.TICKETS, list);
    },

    removeByOwner(username) {
      const list = tickets.all().filter((t) => t.userName !== username);
      return write(KEYS.TICKETS, list);
    },

    renameOwner(oldName, newName) {
      const list = tickets.all();
      list.forEach((t) => {
        if (t.userName === oldName) t.userName = newName;
      });
      return write(KEYS.TICKETS, list);
    },

    /** Seats booked per ticket category (active only). */
    countByClass() {
      const out = {};
      tickets.active().forEach((t) => {
        out[t.category] = (out[t.category] || 0) + t.count;
      });
      return out;
    },

    /** { "Q1 2026": { bookings, seats } } — active only. */
    countByQuarter() {
      const out = {};
      tickets.active().forEach((t) => {
        const d = new Date(t.bookedOn);
        const q = `Q${Math.floor(d.getMonth() / 3) + 1} ${d.getFullYear()}`;
        if (!out[q]) out[q] = { bookings: 0, seats: 0 };
        out[q].bookings += 1;
        out[q].seats += t.count;
      });
      return out;
    },

    /** Seats already sold on one train for one date. */
    seatsTaken(trainId, dateStr) {
      return tickets.active()
        .filter((t) => t.trainId === trainId && String(t.boardingDT).startsWith(dateStr))
        .reduce((sum, t) => sum + t.count, 0);
    },
  };

  /* ===== TRAINS ===== */
  const trains = {
    all() {
      return read(KEYS.TRAINS, []);
    },

    find(id) {
      return trains.all().find((t) => t.id === id) || null;
    },

    /** Trains running a given origin -> destination pair. */
    onRoute(from, to) {
      return trains.all().filter((t) => t.from === from && t.to === to);
    },

    add(data) {
      const record = {
        id: nextId("TR", 5),
        registeredOn: new Date().toISOString(),
        ...data,
      };
      const list = trains.all();
      list.push(record);
      return write(KEYS.TRAINS, list) ? record : null;
    },

    remove(id) {
      return write(KEYS.TRAINS, trains.all().filter((t) => t.id !== id));
    },
  };

  /* ===== SESSION ===== */
  const session = {
    get() {
      return read(KEYS.SESSION, null);
    },

    set(username, role) {
      return write(KEYS.SESSION, {
        username,
        role: role || "customer",
        loginAt: new Date().toISOString(),
      });
    },

    clear() {
      return remove(KEYS.SESSION);
    },

    /** The full user record for whoever is logged in. */
    user() {
      const s = session.get();
      return s ? users.find(s.username) : null;
    },

    isAdmin() {
      const s = session.get();
      return !!s && s.role === "admin";
    },

    isLoggedIn() {
      return session.get() !== null;
    },

    /**
     * Page guard. Call at the top of a portal page.
     *   DB.session.require("admin", "index.html")
     * Redirects and returns false if access is not allowed.
     */
    require(role, redirectTo = "index.html") {
      const s = session.get();
      const ok = s && (!role || s.role === role);
      if (!ok) {
        window.location.href = redirectTo;
        return false;
      }
      return true;
    },
  };

  /* ===== SEED =====
     Creates the default admin on first run so the admin
     portal is reachable before anyone registers.        */
  function seed() {
    if (users.admins().length === 0) {
      users.add({
        username: "adminuser",
        role: "admin",
        email: "[email protected]",
        password: "Admin@123",
        mobile: "9999999999",
      });
    }
  }

  /* ===== MAINTENANCE ===== */
  function exportAll() {
    return {
      users: users.all(),
      tickets: tickets.all(),
      trains: trains.all(),
      session: session.get(),
    };
  }

  function resetAll() {
    Object.values(KEYS).forEach(remove);
    seed();
  }


  /* ===== PUBLIC API ===== */
  return {
    KEYS, users, tickets, trains, session,
    nextId, seed, exportAll, resetAll, isAvailable,
    read, write, remove,
  };
})();

/* Warn once if storage is unusable (private mode, blocked cookies). */
if (!DB.isAvailable()) {
  console.warn("localStorage is unavailable — data will not persist.");
} else {
  DB.seed();
}

// already signed in? skip the login form
(() => {
  const s = DB.session.get();
  if (s) window.location.href = s.role === "admin" ? "admin.html" : "home.html";
})();

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

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

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

  DB.session.set(user.username, user.role);

  const isAdmin = user.role === "admin";
  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 = isAdmin ? "admin.html" : "home.html";
    }, 700);
  }, 1000);

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;
  }


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

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";
});

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="${t.id}">${t.id} — ${t.name} (dep ${t.depart})</option>`);
  });

const train = DB.trains.find(document.getElementById("bk-train").value);
  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);

  const free = train.seats - DB.tickets.seatsTaken(train.id, date);
  if (count > free) {
    setError("bk-count-err", free > 0 ? `Only ${free} seat(s) left.` : "Train is fully booked.");
    return;
  }

  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: `${date} ${train.depart}`,
    arrivalDT: computeArrival(date, train.depart, train.duration),
    count,
  });

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


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


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


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

  if (pwToggle.checked) {
    if (user.password !== document.getElementById("up-current").value) {
      setError("up-current-err", "Current password is incorrect.");
      return;
    }
    patch.password = document.getElementById("up-new").value;
  }

  if (!DB.users.update(user.username, patch)) {
    showToast("Update failed — storage error.", true);
    return;
  }

⚠️Content was pasted as plain text and auto-formatted as a code block. Use the Code Block button in the editor for proper formatting.

← Back to timeline