<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TrainGo — Admin Portal</title>
<link rel="stylesheet" href="home.css" />
<link rel="stylesheet" href="admin.css" />
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
<!-- ===== TOP BAR ===== -->
<header class="topbar">
<div class="topbar__brand">
<span class="brand__icon">🚆</span>
<span class="brand__name">TrainGo</span>
<span class="admin-badge">ADMIN</span>
</div>
<div class="topbar__user">
<span class="user-chip">🛡️ <span id="current-user">admin</span></span>
<button class="btn-logout" id="logout-btn">Logout</button>
</div>
</header>
<!-- ===== TAB NAV ===== -->
<nav class="tabs">
<button class="tab active" data-panel="panel-home">📊 Home</button>
<button class="tab" data-panel="panel-train">🚂 Register a Train</button>
<button class="tab" data-panel="panel-profile">👤 Profile</button>
</nav>
<main class="content">
<!-- ========== HOME ========== -->
<section id="panel-home" class="panel active">
<h1 class="panel__title">Admin Dashboard</h1>
<p class="panel__sub">System-wide booking activity and customer records.</p>
<div class="stat-row">
<div class="stat-card stat-card--yellow">
<div class="stat-card__label">Total Customers</div>
<div class="stat-card__value" id="stat-customers">0</div>
</div>
<div class="stat-card stat-card--green">
<div class="stat-card__label">Active Tickets</div>
<div class="stat-card__value" id="stat-active">0</div>
</div>
<div class="stat-card stat-card--pink">
<div class="stat-card__label">Registered Trains</div>
<div class="stat-card__value" id="stat-trains">0</div>
</div>
</div>
<!-- 1.1 Tickets per class -->
<h2 class="section-head">1.1 — Number of Tickets Booked Per Class</h2>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr><th>Ticket Category</th><th>Tickets Booked</th><th>Share</th></tr>
</thead>
<tbody id="class-agg-body">
<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>
</tbody>
</table>
</div>
<!-- 1.2 Sales per quarter -->
<h2 class="section-head">1.2 — Sales Done Per Quarter</h2>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr><th>Quarter</th><th>Bookings</th><th>Tickets</th></tr>
</thead>
<tbody id="quarter-agg-body">
<tr><td colspan="3" class="empty-row">No bookings yet.</td></tr>
</tbody>
</table>
</div>
<!-- 2. Client table -->
<h2 class="section-head">2 — Customer Records</h2>
<div class="table-wrap table-wrap--scroll">
<table class="data-table">
<thead>
<tr>
<th>2.2.1 ID</th>
<th>2.2.2 Name</th>
<th>2.2.3 Mobile</th>
<th>2.2.4 Tickets Booked</th>
<th>2.2.5 Action</th>
</tr>
</thead>
<tbody id="customers-body">
<tr><td colspan="5" class="empty-row">No customers registered yet.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ========== REGISTER A TRAIN ========== -->
<section id="panel-train" class="panel">
<h1 class="panel__title">Register a Train</h1>
<p class="panel__sub">Registered trains become selectable by customers when booking.</p>
<form id="train-form" novalidate>
<div class="form-grid">
<div class="input-group">
<label for="tr-name">🚂 Name of Train</label>
<input type="text" id="tr-name" placeholder="e.g. Rajdhani Express" />
<span class="error-msg" id="tr-name-err"></span>
</div>
<div class="input-group">
<label for="tr-seats">💺 Number of Seats</label>
<input type="text" id="tr-seats" placeholder="e.g. 480" maxlength="4" inputmode="numeric" />
<span class="error-msg" id="tr-seats-err"></span>
</div>
<div class="input-group">
<label for="tr-from">🚉 From</label>
<select id="tr-from"><option value="">Select station</option></select>
<span class="error-msg" id="tr-from-err"></span>
</div>
<div class="input-group">
<label for="tr-to">🏁 To</label>
<select id="tr-to"><option value="">Select station</option></select>
<span class="error-msg" id="tr-to-err"></span>
</div>
<div class="input-group">
<label for="tr-depart">🕐 Departure Time</label>
<input type="time" id="tr-depart" />
<span class="error-msg" id="tr-depart-err"></span>
</div>
<div class="input-group">
<label for="tr-duration">⏱️ Duration (hours)</label>
<input type="text" id="tr-duration" placeholder="e.g. 16.5" maxlength="5" />
<span class="error-msg" id="tr-duration-err"></span>
</div>
<div class="input-group input-group--wide">
<label for="tr-owner">🏢 Ownership</label>
<select id="tr-owner">
<option value="">Select ownership</option>
<option value="Indian Railways">Indian Railways</option>
<option value="IRCTC">IRCTC</option>
<option value="Private Operator">Private Operator</option>
<option value="State Railway">State Railway</option>
<option value="Joint Venture">Joint Venture</option>
</select>
<span class="error-msg" id="tr-owner-err"></span>
</div>
</div>
<button type="submit" class="btn btn--green">
<span class="btn__text">Register Train</span>
<span class="btn__arrow">→</span>
</button>
</form>
<h2 class="section-head">Registered Trains</h2>
<div class="table-wrap table-wrap--scroll">
<table class="data-table">
<thead>
<tr>
<th>Train ID</th>
<th>Name</th>
<th>Seats</th>
<th>From</th>
<th>To</th>
<th>Departs</th>
<th>Ownership</th>
<th>Action</th>
</tr>
</thead>
<tbody id="trains-body">
<tr><td colspan="8" class="empty-row">No trains registered yet.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ========== PROFILE ========== -->
<section id="panel-profile" class="panel">
<h1 class="panel__title">Profile</h1>
<p class="panel__sub">Update your admin account information.</p>
<form id="profile-form" novalidate>
<div class="form-grid">
<div class="input-group">
<label for="pf-username">👤 Username</label>
<input type="text" id="pf-username" placeholder="admin_username" />
<span class="error-msg" id="pf-username-err"></span>
</div>
<div class="input-group">
<label for="pf-email">✉️ Email Id</label>
<input type="email" id="pf-email" placeholder="[email protected]" />
<span class="error-msg" id="pf-email-err"></span>
</div>
<div class="input-group">
<label for="pf-mobile">📱 Mobile Number</label>
<input type="text" id="pf-mobile" placeholder="10-digit number" maxlength="10" inputmode="numeric" />
<span class="error-msg" id="pf-mobile-err"></span>
</div>
</div>
<label class="checkbox-row">
<input type="checkbox" id="pf-toggle-pw" />
<span>🔐 Update Password</span>
</label>
<div class="form-grid pw-section" id="pf-pw-section">
<div class="input-group">
<label for="pf-current">Current Password</label>
<input type="password" id="pf-current" placeholder="••••••••" />
<span class="error-msg" id="pf-current-err"></span>
</div>
<div class="input-group">
<label for="pf-new">New Password</label>
<input type="password" id="pf-new" placeholder="••••••••" />
<span class="error-msg" id="pf-new-err"></span>
</div>
<div class="input-group">
<label for="pf-confirm">Confirm Password</label>
<input type="password" id="pf-confirm" placeholder="••••••••" />
<span class="error-msg" id="pf-confirm-err"></span>
</div>
</div>
<button type="submit" class="btn btn--yellow">
<span class="btn__text">Save Changes</span>
<span class="btn__arrow">→</span>
</button>
</form>
<h2 class="section-head">Danger Zone</h2>
<div class="danger-zone">
<div>
<strong>Delete this account</strong>
<p>Permanently removes your admin account. This cannot be undone.</p>
</div>
<button class="btn btn--red" id="delete-self-btn">Delete Account</button>
</div>
</section>
</main>
<!-- ===== GENERIC CONFIRM MODAL ===== -->
<div class="modal-backdrop" id="confirm-modal">
<div class="modal">
<div class="modal__accent"></div>
<div class="modal__body">
<h2 class="modal__title" id="confirm-title">Are you sure?</h2>
<p class="modal__text" id="confirm-text"></p>
<div class="modal__actions">
<button class="btn btn--red" id="c
onfirm-yes">Yes, Delete</button>
<button class="btn btn--outline" id="confirm-no">Cancel</button>
</div>
</div>
</div>
</div>
<div id="toast" class="toast" aria-live="polite"></div>
<script src="store.js"></script>
<script src="admin.js"></script>
</body>
</html>
/* =====================================================
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 = {
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'",
};
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();
}