/* =====================================================
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.