Courier Management Frontend
track.html
Courier Management Frontend
track.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ParcelMS — Track Parcel</title>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
<div class="app-shell">
<div class="sidebar" id="sidebar"></div>
<div class="main-area fade-in">
<div class="page-header">
<div class="page-title">🔍 Track Parcel</div>
<div class="page-sub">Enter your Consumer ID to view all your bookings and parcel status</div>
</div>
<!-- Search -->
<div class="card" style="display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;">
<div style="flex:1;min-width:220px;">
<label class="form-label">Consumer ID</label>
<input type="number" id="search-consumer-id" class="form-input" placeholder="e.g. 1" />
</div>
<button id="btn-track" class="btn btn-primary" onclick="doTrack()" style="height:40px;">
🔍 Track
</button>
</div>
<!-- Results -->
<div id="track-results" style="display:none;" class="fade-in">
<div class="card">
<div class="card-title"><span>📋</span> Booking History</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Booking ID</th>
<th>Receiver</th>
<th>Address</th>
<th>Delivery Type</th>
<th>Weight</th>
<th>Pickup Status</th>
<th>Delivery Status</th>
<th>Booked On</th>
</tr>
</thead>
<tbody id="track-tbody"></tbody>
</table>
</div>
</div>
</div>
<div id="track-empty" style="display:none;" class="card">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div class="empty-text">No bookings found for this Consumer ID.</div>
</div>
</div>
<div id="alert-track" class="alert alert-hidden"></div>
</div>
</div>
<div id="toast"></div>
<script src="/js/api.js"></script>
<script>
Session.require();
renderSidebar('track');
async function doTrack() {
const id = document.getElementById('search-consumer-id').value;
if (!id) { showAlert('alert-track','Please enter a Consumer ID.','error'); return; }
setLoading('btn-track', true, '🔍 Track');
document.getElementById('track-results').style.display = 'none';
document.getElementById('track-empty').style.display = 'none';
try {
const res = await CustomerAPI.trackParcel(parseInt(id));
if (res.success && res.data?.length) {
const rows = res.data.map(b => `
<tr>
<td><span style="font-family:'DM Mono',monospace;color:var(--accent);">#${b.bookingId}</span></td>
<td>${b.recName}</td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${b.recAddress}">${b.recAddress}</td>
<td>${deliveryBadge(b.parDeliveryType)}</td>
<td>${b.parWeightGram ? b.parWeightGram + ' g' : '—'}</td>
<td>${statusBadge(b.pickupStatus)}</td>
<td>${statusBadge(b.deliveryStatus)}</td>
<td style="white-space:nowrap;">${b.bookingDate || '—'}</td>
</tr>`).join('');
document.getElementById('track-tbody').innerHTML = rows;
document.getElementById('track-results').style.display = 'block';
} else {
document.getElementById('track-empty').style.display = 'block';
}
} catch(e) { showAlert('alert-track','Server unreachable.','error'); }
finally { setLoading('btn-track', false, '🔍 Track'); }
}
/* Pre-fill Consumer ID if stored */
const user = Session.get();
if (user?.userId) document.getElementById('search-consumer-id').value = user.userId;
document.getElementById('search-consumer-id').addEventListener('keydown', e => {
if (e.key === 'Enter') doTrack();
});
</script>
</body>
</html>
officer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ParcelMS — Officer Dashboard</title>
<link rel="stylesheet" href="./css/styles.css" />
<style>
.modal-overlay {
display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);
z-index:1000;align-items:center;justify-content:center;
}
.modal-overlay.open { display:flex; }
.modal {
background:var(--card);border:1px solid var(--border);border-radius:16px;
padding:28px;width:480px;max-width:94vw;box-shadow:0 20px 60px rgba(0,0,0,0.6);
}
.modal-header { display:flex;justify-content:space-between;align-items:center;margin-bottom:20px; }
.modal-title { font-family:'Syne',sans-serif;font-weight:700;font-size:17px; }
.modal-close { background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer; }
.modal-close:hover { color:var(--text); }
</style>
</head>
<body>
<div class="app-shell">
<div class="sidebar" id="sidebar"></div>
<div class="main-area fade-in">
<div class="page-header">
<div class="page-title">🗂️ Officer Dashboard</div>
<div class="page-sub">Manage all parcel bookings, update pickup/drop and delivery status</div>
</div>
<!-- Stat cards -->
<div class="stats-row" id="stats-row">
<div class="stat-card stat-orange">
<div class="stat-num" id="stat-total">—</div>
<div class="stat-label">Total Bookings</div>
<div class="stat-icon">📦</div>
</div>
<div class="stat-card stat-blue">
<div class="stat-num" id="stat-pending">—</div>
<div class="stat-label">Pending Delivery</div>
<div class="stat-icon">🕐</div>
</div>
<div class="stat-card stat-green">
<div class="stat-num" id="stat-delivered">—</div>
<div class="stat-label">Delivered</div>
<div class="stat-icon">✅</div>
</div>
</div>
<!-- All bookings table -->
<div class="card">
<div class="card-title" style="justify-content:space-between;">
<span><span>📋</span> All Bookings</span>
<button class="btn btn-secondary btn-sm" onclick="loadBookings()">↺ Refresh</button>
</div>
<div id="alert-officer" class="alert alert-hidden"></div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Booking ID</th>
<th>Consumer ID</th>
<th>Receiver</th>
<th>Delivery Type</th>
<th>Weight</th>
<th>Pickup Status</th>
<th>Delivery Status</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="bookings-tbody">
<tr><td colspan="9" style="text-align:center;padding:32px;color:var(--muted);">
<span class="spinner"></span> Loading bookings...
</td></tr>
</tbody>
</table>
</div>
</div>
</div><!-- /main-area -->
</div>
<!-- ── Update Pickup/Drop Modal ── -->
<div class="modal-overlay" id="modal-pickup">
<div class="modal">
<div class="modal-header">
<div class="modal-title">✏️ Update Pickup & Drop</div>
<button class="modal-close" onclick="closeModal('modal-pickup')">✕</button>
</div>
<div id="alert-pickup" class="alert alert-hidden"></div>
<input type="hidden" id="pickup-booking-id" />
<div class="form-group">
<label class="form-label">Pickup Time</label>
<input type="datetime-local" id="pickup-time" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">Dropoff Time</label>
<input type="datetime-local" id="dropoff-time" class="form-input" />
</div>
<div style="display:flex;gap:10px;margin-top:8px;">
<button id="btn-save-pickup" class="btn btn-primary" onclick="savePickupDrop()">Save Changes</button>
<button class="btn btn-secondary" onclick="closeModal('modal-pickup')">Cancel</button>
</div>
</div>
</div>
<!-- ── Update Delivery Status Modal ── -->
<div class="modal-overlay" id="modal-delivery">
<div class="modal">
<div class="modal-header">
<div class="modal-title">🚚 Update Delivery Status</div>
<button class="modal-close" onclick="closeModal('modal-delivery')">✕</button>
</div>
<div id="alert-delivery" class="alert alert-hidden"></div>
<input type="hidden" id="delivery-booking-id" />
<div class="form-group">
<label class="form-label">Booking ID</label>
<input type="text" id="delivery-booking-display" class="form-input" readonly
style="background:var(--bg);cursor:not-allowed;color:var(--muted);" />
</div>
<div class="form-group">
<label class="form-label">Delivery Status</label>
<select id="delivery-status" class="form-select">
<option value="PENDING">Pending</option>
<option value="IN_TRANSIT">In Transit</option>
<option value="OUT_FOR_DELIVERY">Out for Delivery</option>
<option value="DELIVERED">Delivered</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div style="display:flex;gap:10px;margin-top:8px;">
<button id="btn-save-delivery" class="btn btn-primary" onclick="saveDelivery()">Update Status</button>
<button class="btn btn-secondary" onclick="closeModal('modal-delivery')">Cancel</button>
</div>
</div>
</div>
<div id="toast"></div>
<script src="/js/api.js"></script>
<script>
Session.require();
renderSidebar('all-bookings');
loadBookings();
async function loadBookings() {
document.getElementById('bookings-tbody').innerHTML =
`<tr><td colspan="9" style="text-align:center;padding:32px;color:var(--muted);"><span class="spinner"></span> Loading...</td></tr>`;
try {
const res = await OfficerAPI.viewAllBookings();
if (res.success && res.data?.length) {
const total = res.data.length;
const pending = res.data.filter(b => b.deliveryStatus !== 'DELIVERED').length;
const delivered = res.data.filter(b => b.deliveryStatus === 'DELIVERED').length;
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-pending').textContent = pending;
document.getElementById('stat-delivered').textContent = delivered;
document.getElementById('bookings-tbody').innerHTML = res.data.map(b => `
<tr>
<td><span style="font-family:'DM Mono',monospace;color:var(--accent);">#${b.bookingId}</span></td>
<td><span style="font-family:'DM Mono',monospace;">${b.consumerId}</span></td>
<td>${b.recName}</td>
<td>${deliveryBadge(b.parDeliveryType)}</td>
<td>${b.parWeightGram ? b.parWeightGram+' g' : '—'}</td>
<td>${statusBadge(b.pickupStatus)}</td>
<td>${statusBadge(b.deliveryStatus)}</td>
<td style="white-space:nowrap;">${b.bookingDate||'—'}</td>
<td>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="btn btn-secondary btn-sm" onclick="openPickupModal(${b.bookingId})">✏️ Pickup</button>
<button class="btn btn-secondary btn-sm" onclick="openDeliveryModal(${b.bookingId}, '${b.deliveryStatus}')">🚚 Delivery</button>
</div>
</td>
</tr>`).join('');
} else {
document.getElementById('bookings-tbody').innerHTML =
`<tr><td colspan="9"><div class="empty-state"><div class="empty-icon">📭</div><div class="empty-text">No bookings found.</div></div></td></tr>`;
}
} catch(e) {
showAlert('alert-officer','Failed to load bookings.','error');
}
}
/* Pickup/Drop Modal */
function openPickupModal(bookingId) {
document.getElementById('pickup-booking-id').value = bookingId;
document.getElementById('alert-pickup').style.display = 'none';
document.getElementById('modal-pickup').classList.add('open');
}
async function savePickupDrop() {
const id = document.getElementById('pickup-booking-id').value;
const pickup = document.getElementById('pickup-time').value.replace('T',' ') + ':00';
const dropoff = document.getElementById('dropoff-time').value.replace('T',' ') + ':00';
if (!document.getElementById('pickup-time').value || !document.getElementById('dropoff-time').value)
{ showAlert('alert-pickup','Both pickup and dropoff times are required.','error'); return; }
setLoading('btn-save-pickup',true,'Save Changes');
try {
const res = await OfficerAPI.updatePickupDrop(id, pickup, dropoff);
if (res.success) {
showToast(res.message);
closeModal('modal-pickup');
loadBookings();
} else { showAlert('alert-pickup', res.message, 'error'); }
} catch(e) { showAlert('alert-pickup','Server unreachable.','error'); }
finally { setLoading('btn-save-pickup',false,'Save Changes'); }
}
/* Delivery Status Modal */
function openDeliveryModal(bookingId, currentStatus) {
document.getElementById('delivery-booking-id').value = bookingId;
document.getElementById('delivery-booking-display').value = '#' + bookingId;
document.getElementById('delivery-status').value = currentStatus || 'PENDING';
document.getElementById('alert-delivery').style.display = 'none';
document.getElementById('modal-delivery').classList.add('open');
}
async function saveDelivery() {
const id = document.getElementById('delivery-booking-id').value;
const status = document.getElementById('delivery-status').value;
setLoading('btn-save-delivery',true,'Update Status');
try {
const res = await OfficerAPI.updateDelivery(id, status);
if (res.success) {
showToast(res.message);
closeModal('modal-delivery');
loadBookings();
} else { showAlert('alert-delivery', res.message, 'error'); }
} catch(e) { showAlert('alert-delivery','Server unreachable.','error'); }
finally { setLoading('btn-save-delivery',false,'Update Status'); }
}
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
/* Close on backdrop click */
document.querySelectorAll('.modal-overlay').forEach(m => {
m.addEventListener('click', e => { if (e.target === m) m.classList.remove('open'); });
});
</script>
</body>
</html>
Login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ParcelMS — Login</title>
<link rel="stylesheet" href="./css/styles.css" />
<style>
.register-tabs { display:flex; gap:4px; margin-bottom:16px; padding:4px;
background:var(--surface); border-radius:8px; }
.reg-tab { flex:1; padding:7px; text-align:center; font-size:12px; font-weight:600;
cursor:pointer; border-radius:6px; transition:all 0.15s; color:var(--muted);
border:none; background:none; font-family:'Outfit',sans-serif; }
.reg-tab.active { background:var(--card); color:var(--text); box-shadow:0 2px 8px rgba(0,0,0,0.3); }
</style>
</head>
<body>
<div class="auth-page">
<div class="auth-bg"></div>
<div class="auth-card fade-in">
<div class="auth-logo">
<div class="auth-logo-icon">📦</div>
<div>
<div class="auth-title">ParcelMS</div>
<div class="auth-title-sub">Parcel Delivery Management System</div>
</div>
</div>
<!-- Main tabs: Login | Register -->
<div class="auth-tabs">
<button class="auth-tab active" onclick="switchTab('login')">Login</button>
<button class="auth-tab" onclick="switchTab('register')">Register</button>
</div>
<!-- ── LOGIN TAB ── -->
<div id="tab-login" class="tab-content active">
<div id="alert-login" class="alert alert-hidden"></div>
<div class="form-group">
<label class="form-label">Email Address</label>
<input type="email" id="login-email" class="form-input" placeholder="you@example.com" />
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="login-password" class="form-input" placeholder="••••••••" />
</div>
<button id="btn-login" class="btn btn-primary btn-full" onclick="doLogin()">Login</button>
<div class="auth-footer">Don't have an account?
<span style="color:var(--accent);cursor:pointer;" onclick="switchTab('register')">Register here</span>
</div>
</div>
<!-- ── REGISTER TAB ── -->
<div id="tab-register" class="tab-content">
<div id="alert-register" class="alert alert-hidden"></div>
<!-- Sub-tabs: Customer | Admin -->
<div class="register-tabs">
<button class="reg-tab active" onclick="switchRegTab('customer')">Customer</button>
<button class="reg-tab" onclick="switchRegTab('admin')">Admin</button>
</div>
<!-- Customer Registration -->
<div id="reg-customer">
<div class="form-row">
<div class="form-group">
<label class="form-label">Full Name</label>
<input type="text" id="reg-name" class="form-input" placeholder="Aarav Shah" />
</div>
<div class="form-group">
<label class="form-label">Mobile</label>
<input type="tel" id="reg-mobile" class="form-input" placeholder="9876543210" />
</div>
</div>
<div class="form-group">
<label class="form-label">Email Address</label>
<input type="email" id="reg-email" class="form-input" placeholder="aarav@example.com" />
</div>
<div class="form-group">
<label class="form-label">Address</label>
<input type="text" id="reg-address" class="form-input" placeholder="Nagpur, Maharashtra" />
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="reg-password" class="form-input" placeholder="Min 6 characters" />
</div>
<button id="btn-reg-customer" class="btn btn-primary btn-full" onclick="doRegisterCustomer()">
Create Account
</button>
</div>
<!-- Admin Registration -->
<div id="reg-admin" style="display:none;">
<div class="alert alert-info" style="margin-bottom:16px;">
<span class="alert-icon">ℹ</span>
Admin accounts have exclusive booking management access.
</div>
<div class="form-group">
<label class="form-label">Admin Email</label>
<input type="email" id="admin-email" class="form-input" placeholder="admin@googel.com" />
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="admin-password" class="form-input" placeholder="Strong password" />
</div>
<button id="btn-reg-admin" class="btn btn-primary btn-full" onclick="doRegisterAdmin()">
Register Admin
</button>
</div>
<div class="auth-footer">Already registered?
<span style="color:var(--accent);cursor:pointer;" onclick="switchTab('login')">Login here</span>
</div>
</div>
</div><!-- /auth-card -->
</div>
<div id="toast"></div>
<script src="/js/api.js"></script>
<script>
/* Redirect if already logged in */
(() => {
const user = Session.get();
if (user) window.location.href = user.role === 'ADMIN' ? '/officer.html' : '/customer.html';
})();
function switchTab(tab) {
['login','register'].forEach(t => {
document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
document.querySelectorAll('.auth-tab').forEach((el,i) => {
el.classList.toggle('active', (i===0 && tab==='login') || (i===1 && tab==='register'));
});
});
}
function switchRegTab(tab) {
document.getElementById('reg-customer').style.display = tab === 'customer' ? 'block' : 'none';
document.getElementById('reg-admin').style.display = tab === 'admin' ? 'block' : 'none';
document.querySelectorAll('.reg-tab').forEach((el,i) => {
el.classList.toggle('active', (i===0 && tab==='customer') || (i===1 && tab==='admin'));
});
}
async function doLogin() {
const email = document.getElementById('login-email').value.trim();
const password = document.getElementById('login-password').value;
if (!email || !password) { showAlert('alert-login','Email and password required.','error'); return; }
setLoading('btn-login', true, 'Login');
try {
const res = await AuthAPI.login(email, password);
if (res.success) {
Session.save(res.data);
showToast('Logged in successfully!');
setTimeout(() => {
window.location.href = res.data.role === 'ADMIN' ? '/officer.html' : '/customer.html';
}, 800);
} else {
showAlert('alert-login', res.message, 'error');
}
} catch(e) { showAlert('alert-login','Server unreachable. Check if backend is running.','error'); }
finally { setLoading('btn-login', false, 'Login'); }
}
async function doRegisterCustomer() {
const data = {
name: document.getElementById('reg-name').value.trim(),
email: document.getElementById('reg-email').value.trim(),
mobile: document.getElementById('reg-mobile').value.trim(),
address: document.getElementById('reg-address').value.trim(),
password: document.getElementById('reg-password').value,
};
if (!data.name || !data.email || !data.password)
{ showAlert('alert-register','Name, email and password are required.','error'); return; }
setLoading('btn-reg-customer', true, 'Create Account');
try {
const res = await AuthAPI.registerCustomer(data);
if (res.success) { showAlert('alert-register', res.message, 'success'); }
else { showAlert('alert-register', res.message, 'error'); }
} catch(e) { showAlert('alert-register','Server unreachable.','error'); }
finally { setLoading('btn-reg-customer', false, 'Create Account'); }
}
async function doRegisterAdmin() {
const email = document.getElementById('admin-email').value.trim();
const password = document.getElementById('admin-password').value;
if (!email || !password) { showAlert('alert-register','Email and password required.','error'); return; }
setLoading('btn-reg-admin', true, 'Register Admin');
try {
const res = await AuthAPI.registerAdmin(email, password);
if (res.success) { showAlert('alert-register', res.message + ' You can now login.', 'success'); }
else { showAlert('alert-register', res.message, 'error'); }
} catch(e) { showAlert('alert-register','Server unreachable.','error'); }
finally { setLoading('btn-reg-admin', false, 'Register Admin'); }
}
/* Enter key triggers login */
document.addEventListener('keydown', e => {
if (e.key === 'Enter' && document.getElementById('tab-login').classList.contains('active'))
doLogin();
});
</script>
</body>
</html>
customer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ParcelMS — Book Service</title>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
<div class="app-shell">
<!-- Sidebar -->
<div class="sidebar" id="sidebar"></div>
<!-- Main -->
<div class="main-area fade-in">
<div class="page-header">
<div class="page-title">📦 Book a Service</div>
<div class="page-sub">Fill in receiver and parcel details to create a booking</div>
</div>
<div id="alert-book" class="alert alert-hidden"></div>
<form onsubmit="doBook(event)">
<div class="grid-2">
<!-- Receiver Details -->
<div class="card">
<div class="card-title"><span>👤</span> Receiver Details</div>
<div class="form-group">
<label class="form-label">Receiver Name</label>
<input type="text" id="rec-name" class="form-input" placeholder="Rahul Sharma" required />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Mobile</label>
<input type="tel" id="rec-mobile" class="form-input" placeholder="9876543210" required />
</div>
<div class="form-group">
<label class="form-label">PIN Code</label>
<input type="text" id="rec-pin" class="form-input" placeholder="411001" maxlength="6" required />
</div>
</div>
<div class="form-group">
<label class="form-label">Address</label>
<textarea id="rec-address" class="form-textarea" placeholder="MG Road, Pune, Maharashtra" required></textarea>
</div>
</div>
<!-- Parcel Details -->
<div class="card">
<div class="card-title"><span>📬</span> Parcel Details</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Weight (grams)</label>
<input type="number" id="par-weight" class="form-input" placeholder="500" min="1" />
</div>
<div class="form-group">
<label class="form-label">Delivery Type</label>
<select id="par-delivery-type" class="form-select">
<option value="STANDARD">Standard</option>
<option value="EXPRESS">Express</option>
<option value="OVERNIGHT">Overnight</option>
<option value="ECONOMY">Economy</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Contents Description</label>
<input type="text" id="par-contents" class="form-input" placeholder="e.g. Books, Clothes, Electronics" />
</div>
<div class="form-group">
<label class="form-label">Packing Preference</label>
<select id="par-packing" class="form-select">
<option value="STANDARD_BOX">Standard Box</option>
<option value="BUBBLE_WRAP">Bubble Wrap</option>
<option value="FRAGILE_PACK">Fragile Pack</option>
<option value="NO_PACKING">No Packing</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Pickup Time</label>
<input type="datetime-local" id="par-pickup" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">Dropoff Time</label>
<input type="datetime-local" id="par-dropoff" class="form-input" />
</div>
</div>
</div>
<!-- Consumer ID + Submit -->
<div class="card col-span-2" style="display:flex;align-items:center;justify-content:space-between;gap:20px;padding:20px 24px;">
<div style="flex:1;max-width:320px;">
<label class="form-label">Your Consumer ID</label>
<input type="number" id="consumer-id" class="form-input" placeholder="e.g. 1" required />
<div style="font-size:11px;color:var(--muted);margin-top:6px;">
Consumer ID is shown after login. Check your profile or use User ID from registration.
</div>
</div>
<button type="submit" id="btn-book" class="btn btn-primary" style="min-width:180px;justify-content:center;padding:12px 28px;">
🚀 Book Service
</button>
</div>
</div>
</form>
<!-- Confirmation -->
<div id="booking-confirm" style="display:none;" class="card fade-in">
<div class="card-title"><span>✅</span> Booking Confirmed</div>
<div id="booking-confirm-body"></div>
</div>
</div><!-- /main-area -->
</div>
<div id="toast"></div>
<script src="/js/api.js"></script>
<script>
const user = Session.require();
renderSidebar('book');
function fmtDT(dtLocal) {
if (!dtLocal) return null;
return dtLocal.replace('T', ' ') + ':00';
}
async function doBook(e) {
e.preventDefault();
const consumerId = parseInt(document.getElementById('consumer-id').value);
if (!consumerId) { showAlert('alert-book','Consumer ID is required.','error'); return; }
const booking = {
consumerId,
recName: document.getElementById('rec-name').value.trim(),
recAddress: document.getElementById('rec-address').value.trim(),
recPin: document.getElementById('rec-pin').value.trim(),
recMobile: document.getElementById('rec-mobile').value.trim(),
parWeightGram: parseFloat(document.getElementById('par-weight').value) || null,
parContentsDescription: document.getElementById('par-contents').value.trim(),
parDeliveryType: document.getElementById('par-delivery-type').value,
parPackingPreference: document.getElementById('par-packing').value,
parPickupTime: fmtDT(document.getElementById('par-pickup').value) || null,
parDropoffTime: fmtDT(document.getElementById('par-dropoff').value) || null,
};
setLoading('btn-book', true, '🚀 Book Service');
try {
const res = await CustomerAPI.bookService(booking);
if (res.success) {
showToast(res.message, 'success');
document.getElementById('booking-confirm').style.display = 'block';
document.getElementById('booking-confirm-body').innerHTML = `
<div class="detail-row"><span class="detail-key">Status</span><span class="detail-val">${statusBadge('PENDING')}</span></div>
<div class="detail-row"><span class="detail-key">Booking Ref</span><span class="detail-val" style="color:var(--accent);font-family:'DM Mono',monospace;">${res.data || '—'}</span></div>
<div class="detail-row"><span class="detail-key">Receiver</span><span class="detail-val">${booking.recName}</span></div>
<div class="detail-row"><span class="detail-key">Address</span><span class="detail-val">${booking.recAddress}</span></div>
<div class="detail-row"><span class="detail-key">Delivery Type</span><span class="detail-val">${deliveryBadge(booking.parDeliveryType)}</span></div>
<div class="detail-row"><span class="detail-key">Weight</span><span class="detail-val">${booking.parWeightGram || '—'} g</span></div>
<div style="margin-top:16px;">
<a href="/track.html" class="btn btn-secondary">🔍 Track this Parcel</a>
</div>`;
document.getElementById('booking-confirm').scrollIntoView({ behavior:'smooth' });
} else {
showAlert('alert-book', res.message, 'error');
}
} catch(err) { showAlert('alert-book','Server unreachable.','error'); }
finally { setLoading('btn-book', false, '🚀 Book Service'); }
}
</script>
</body>
</html>
style.css
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:ital,wght@0,400;0,500;1,400&family=Outfit:wght@300;400;500;600;700&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0c10;
--surface: #111318;
--card: #181c24;
--border: #252a35;
--accent: #f97316;
--accent2: #fb923c;
--text: #e8eaf0;
--muted: #6b7280;
--success: #22c55e;
--danger: #ef4444;
--warning: #eab308;
--info: #3b82f6;
--radius: 12px;
}
html { scroll-behavior: smooth; }
body { font-family: 'Outfit', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ── App Shell ── */
.app-shell { display: flex; min-height: 100vh; }
/* ── Sidebar ── */
.sidebar {
width: 240px; min-height: 100vh; background: var(--surface);
border-right: 1px solid var(--border); display: flex;
flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 100;
}
.main-area { margin-left: 240px; flex: 1; padding: 36px; }
/* ── Logo ── */
.logo { padding: 22px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.logo-icon {
width: 38px; height: 38px; background: var(--accent); border-radius: 8px;
display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0;
}
.logo-text { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 14px; line-height: 1.3; }
.logo-sub { font-size: 10px; color: var(--muted); }
/* ── Nav ── */
.nav { padding: 12px 0; flex: 1; overflow-y: auto; }
.nav-section {
padding: 10px 20px 4px; font-size: 9px; text-transform: uppercase;
letter-spacing: 1.8px; color: var(--muted); font-family: 'DM Mono', monospace;
}
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 10px 20px;
cursor: pointer; font-size: 13.5px; color: var(--muted);
border-left: 3px solid transparent; transition: all 0.15s; text-decoration: none;
}
.nav-item:hover { color: var(--text); background: rgba(255,255,255,0.04); }
.nav-item.active { color: var(--accent); border-left-color: var(--accent); background: rgba(249,115,22,0.08); }
.nav-icon { width: 18px; text-align: center; font-size: 15px; }
/* ── User Badge ── */
.user-badge {
margin: 12px; padding: 12px; background: var(--card);
border-radius: 10px; border: 1px solid var(--border);
}
.user-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-role { font-size: 11px; color: var(--muted); margin-top: 2px; }
.role-pill {
display: inline-block; padding: 2px 8px; border-radius: 20px;
font-size: 10px; font-family: 'DM Mono', monospace; margin-top: 6px;
}
.role-admin { background: rgba(249,115,22,0.15); color: var(--accent); }
.role-customer { background: rgba(59,130,246,0.15); color: var(--info); }
/* ── Logout Button ── */
.logout-btn {
margin: 0 12px 16px; padding: 10px 14px; background: none;
border: 1px solid var(--border); border-radius: 8px; color: var(--muted);
font-size: 13px; cursor: pointer; display: flex; align-items: center;
gap: 8px; transition: all 0.15s; font-family: 'Outfit', sans-serif; width: calc(100% - 24px);
}
.logout-btn:hover { border-color: var(--danger); color: var(--danger); }
/* ── Page Header ── */
.page-header { margin-bottom: 28px; }
.page-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 26px; }
.page-sub { color: var(--muted); font-size: 14px; margin-top: 4px; }
/* ── Cards ── */
.card {
background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 24px; margin-bottom: 20px;
}
.card-title {
font-family: 'Syne', sans-serif; font-weight: 700; font-size: 15px;
margin-bottom: 20px; display: flex; align-items: center; gap: 8px;
}
.card-title span { font-size: 18px; }
/* ── Stat Cards ── */
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
.stat-card {
background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 20px; position: relative; overflow: hidden;
}
.stat-card::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; }
.stat-orange::before { background: var(--accent); }
.stat-blue::before { background: var(--info); }
.stat-green::before { background: var(--success); }
.stat-num { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 30px; }
.stat-label { font-size: 12px; color: var(--muted); margin-top: 4px; }
.stat-icon { position: absolute; right: 16px; top: 16px; font-size: 28px; opacity: 0.12; }
/* ── Grid ── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.col-span-2 { grid-column: span 2; }
/* ── Forms ── */
.form-group { margin-bottom: 16px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-label {
display: block; font-size: 11px; color: var(--muted); margin-bottom: 6px;
font-family: 'DM Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px;
}
.form-input, .form-select, .form-textarea {
width: 100%; padding: 10px 14px; background: var(--surface);
border: 1px solid var(--border); border-radius: 8px; color: var(--text);
font-family: 'Outfit', sans-serif; font-size: 14px; transition: border-color 0.2s; outline: none;
}
.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--accent); }
.form-select option { background: var(--card); }
.form-textarea { resize: vertical; min-height: 80px; }
/* ── Buttons ── */
.btn {
padding: 10px 22px; border: none; border-radius: 8px; font-size: 14px;
font-family: 'Outfit', sans-serif; font-weight: 600; cursor: pointer;
transition: all 0.15s; display: inline-flex; align-items: center; gap: 8px;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent2); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(249,115,22,0.35); }
.btn-secondary { background: var(--border); color: var(--text); }
.btn-secondary:hover { background: #2f3542; }
.btn-danger { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.btn-sm { padding: 6px 14px; font-size: 12px; }
.btn-full { width: 100%; justify-content: center; padding: 12px; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
/* ── Table ── */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
padding: 10px 14px; text-align: left; font-family: 'DM Mono', monospace;
font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted);
border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody td { padding: 12px 14px; border-bottom: 1px solid rgba(37,42,53,0.6); vertical-align: middle; }
tbody tr:hover td { background: rgba(255,255,255,0.02); }
tbody tr:last-child td { border-bottom: none; }
/* ── Badges ── */
.badge {
display: inline-block; padding: 3px 10px; border-radius: 20px;
font-size: 10px; font-family: 'DM Mono', monospace; font-weight: 500; white-space: nowrap;
}
.badge-green { background: rgba(34,197,94,0.12); color: var(--success); }
.badge-orange { background: rgba(249,115,22,0.12); color: var(--accent); }
.badge-blue { background: rgba(59,130,246,0.12); color: var(--info); }
.badge-yellow { background: rgba(234,179,8,0.12); color: var(--warning); }
.badge-red { background: rgba(239,68,68,0.12); color: var(--danger); }
.badge-muted { background: rgba(107,114,128,0.1); color: var(--muted); }
/* ── Alerts ── */
.alert { padding: 12px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; display: flex; align-items: flex-start; gap: 10px; }
.alert-success { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.25); color: #4ade80; }
.alert-error { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); color: #f87171; }
.alert-info { background: rgba(59,130,246,0.08); border: 1px solid rgba(59,130,246,0.25); color: #60a5fa; }
.alert-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.alert-hidden { display: none; }
/* ── Auth Page ── */
.auth-page {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: var(--bg); position: relative; overflow: hidden;
}
.auth-bg {
position: absolute; inset: 0;
background: radial-gradient(ellipse 60% 60% at 30% 50%, rgba(249,115,22,0.07) 0%, transparent 70%),
radial-gradient(ellipse 50% 50% at 75% 20%, rgba(59,130,246,0.04) 0%, transparent 60%);
}
.auth-card {
background: var(--card); border: 1px solid var(--border); border-radius: 16px;
padding: 40px; width: 440px; position: relative; z-index: 1;
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.auth-logo { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }
.auth-logo-icon {
width: 44px; height: 44px; background: var(--accent); border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.auth-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 20px; }
.auth-title-sub { font-size: 11px; color: var(--muted); margin-top: 1px; }
.auth-tabs { display: flex; gap: 4px; margin-bottom: 24px; padding: 4px; background: var(--surface); border-radius: 10px; }
.auth-tab {
flex: 1; padding: 9px; text-align: center; font-size: 13px; font-weight: 600;
cursor: pointer; border-radius: 7px; transition: all 0.15s; color: var(--muted);
border: none; background: none; font-family: 'Outfit', sans-serif;
}
.auth-tab.active { background: var(--card); color: var(--text); box-shadow: 0 2px 8px rgba(0,0,0,0.3); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.divider { height: 1px; background: var(--border); margin: 20px 0; }
.auth-footer { text-align: center; font-size: 12px; color: var(--muted); margin-top: 20px; }
/* ── Detail Rows ── */
.detail-row { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
.detail-row:last-child { border-bottom: none; }
.detail-key { color: var(--muted); font-family: 'DM Mono', monospace; font-size: 11px; text-transform: uppercase; }
.detail-val { font-weight: 500; text-align: right; }
/* ── Spinner ── */
.spinner {
display: inline-block; width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.25); border-top-color: #fff;
border-radius: 50%; animation: spin 0.65s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Animations ── */
.fade-in { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
/* ── Empty State ── */
.empty-state { text-align: center; padding: 48px 24px; color: var(--muted); }
.empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.4; }
.empty-text { font-size: 14px; }
/* ── Toast ── */
#toast {
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
padding: 12px 20px; border-radius: 10px; font-size: 13px; font-weight: 500;
display: flex; align-items: center; gap: 8px; min-width: 260px;
box-shadow: 0 8px 30px rgba(0,0,0,0.4); transform: translateY(80px);
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1); opacity: 0;
}
#toast.show { transform: translateY(0); opacity: 1; }
#toast.toast-success { background: #14532d; border: 1px solid rgba(34,197,94,0.4); color: #4ade80; }
#toast.toast-error { background: #450a0a; border: 1px solid rgba(239,68,68,0.4); color: #f87171; }
/* ── Section Divider ── */
.section-gap { margin-top: 28px; }
/* ── Responsive ── */
@media (max-width: 900px) {
.sidebar { transform: translateX(-100%); }
.main-area { margin-left: 0; padding: 20px; }
.stats-row { grid-template-columns: 1fr 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.form-row { grid-template-columns: 1fr; }
}
/* ============================================================
api.js — Shared API utility for Parcel Delivery System
============================================================ */
const BASE_URL = 'http://localhost:8080/api';
/* ── HTTP helper ── */
async function apiCall(endpoint, method = 'GET', body = null) {
const options = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(BASE_URL + endpoint, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
/* ── Auth convenience wrappers ── */
const AuthAPI = {
login: (email, password) => apiCall('/validateLogin', 'POST', { email, password }),
registerCustomer:(data) => apiCall('/registerCustomer','POST', data),
registerAdmin: (email, password) => apiCall('/registerAdmin', 'POST', { email, password }),
};
/* ── Customer convenience wrappers ── */
const CustomerAPI = {
bookService: (booking) => apiCall('/viewbookService', 'POST', booking),
trackParcel: (consumerId) => apiCall(`/trackParcelStatus/${consumerId}`),
};
/* ── Officer convenience wrappers ── */
const OfficerAPI = {
viewAllBookings: () => apiCall('/officer/viewbookService'),
viewByDate: (date) => apiCall(`/officer/viewBookingHistory?date=${date}`),
updatePickupDrop:(bookingId, pickup, dropoff) =>
apiCall('/officer/updatepickupanddrop', 'PUT', {
bookingId: String(bookingId),
parPickupTime: pickup,
parDropoffTime: dropoff
}),
updateDelivery: (bookingId, status) =>
apiCall('/officer/updateDeliveryStatus', 'PUT', {
bookingId: String(bookingId),
deliveryStatus: status
}),
};
/* ── Session helpers ── */
const Session = {
save(user) { sessionStorage.setItem('parcel_user', JSON.stringify(user)); },
get() { return JSON.parse(sessionStorage.getItem('parcel_user') || 'null'); },
clear() { sessionStorage.removeItem('parcel_user'); },
isAdmin() { return this.get()?.role === 'ADMIN'; },
require() {
if (!this.get()) { window.location.href = '/login.html'; return null; }
return this.get();
}
};
/* ── Toast notification ── */
function showToast(message, type = 'success') {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
toast.className = `toast-${type}`;
toast.innerHTML = `<span>${type === 'success' ? '✓' : '✕'}</span> ${message}`;
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => toast.classList.remove('show'), 3500);
}
/* ── Alert helper ── */
function showAlert(alertId, message, type = 'success') {
const el = document.getElementById(alertId);
if (!el) return;
el.className = `alert alert-${type}`;
el.innerHTML = `<span class="alert-icon">${type === 'success' ? '✓' : '✕'}</span> ${message}`;
el.style.display = 'flex';
setTimeout(() => el.style.display = 'none', 5000);
}
/* ── Loading state ── */
function setLoading(btnId, loading, text = 'Submit') {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = loading;
btn.innerHTML = loading
? `<span class="spinner"></span> Loading...`
: text;
}
/* ── Status badge ── */
function statusBadge(status) {
if (!status) return '<span class="badge badge-muted">—</span>';
const s = status.toUpperCase();
if (s === 'DELIVERED') return `<span class="badge badge-green">${status}</span>`;
if (s === 'IN_TRANSIT' || s === 'UPDATED') return `<span class="badge badge-blue">${status}</span>`;
if (s === 'OUT_FOR_DELIVERY') return `<span class="badge badge-orange">${status}</span>`;
if (s === 'PENDING') return `<span class="badge badge-yellow">${status}</span>`;
if (s === 'CANCELLED') return `<span class="badge badge-red">${status}</span>`;
return `<span class="badge badge-muted">${status}</span>`;
}
/* ── Date formatter ── */
function fmtDate(val) {
if (!val) return '—';
return new Date(val).toLocaleString('en-IN', { dateStyle:'medium', timeStyle:'short' });
}
/* ── Delivery type badge ── */
function deliveryBadge(type) {
if (!type) return '—';
const t = type.toUpperCase();
if (t === 'EXPRESS') return `<span class="badge badge-orange">EXPRESS</span>`;
return `<span class="badge badge-muted">${type}</span>`;
}
/* ── Render shared sidebar ── */
function renderSidebar(activePage) {
const user = Session.get();
if (!user) return;
const isAdmin = user.role === 'ADMIN';
const customerLinks = `
<p class="nav-section">Customer</p>
<a href="/customer.html" class="nav-item ${activePage==='book'?'active':''}">
<span class="nav-icon">📦</span> Book Service
</a>
<a href="/track.html" class="nav-item ${activePage==='track'?'active':''}">
<span class="nav-icon">🔍</span> Track Parcel
</a>`;
const officerLinks = `
<p class="nav-section">Officer</p>
<a href="/officer.html" class="nav-item ${activePage==='all-bookings'?'active':''}">
<span class="nav-icon">📋</span> All Bookings
</a>
<a href="/officer-update.html" class="nav-item ${activePage==='update'?'active':''}">
<span class="nav-icon">✏️</span> Update Booking
</a>
<a href="/officer-history.html" class="nav-item ${activePage==='history'?'active':''}">
<span class="nav-icon">🗓️</span> Booking History
</a>`;
const sidebarEl = document.getElementById('sidebar');
if (!sidebarEl) return;
sidebarEl.innerHTML = `
<div class="logo">
<div class="logo-icon">📦</div>
<div>
<div class="logo-text">ParcelMS</div>
<div class="logo-sub">Management System</div>
</div>
</div>
<nav class="nav">
${isAdmin ? officerLinks + customerLinks : customerLinks}
</nav>
<div class="user-badge">
<div class="user-name">${user.email}</div>
<div class="user-role">Logged in as</div>
<span class="role-pill ${isAdmin ? 'role-admin' : 'role-customer'}">${user.role}</span>
</div>
<button class="logout-btn" onclick="logout()">
<span>⇤</span> Logout
</button>`;
}
function logout() {
Session.clear();
window.location.href = '/login.html';
} 메타데이터
- post_id
- c2d9c7d48189
- slug
- courier-management-frontend-c2d9c7d48189
- url
- https://medium.com/@LordKage/courier-management-frontend-c2d9c7d48189
- canonical_url
- https://medium.com/@LordKage/courier-management-frontend-c2d9c7d48189
- author_url
- https://medium.com/@LordKage
- status
- ok
- fetched_at
- 2026-06-16 19:09:56