← Back to list

Courier Management Frontend

track.html

STRIKERFLASH · 2026-05-21 18:00 · 0 claps · 19.2 min read
#coding-projects #coding
Open on Medium ↗
Wiki topics: BIZ · Business Strategy 💻 · Programming 🌐 · Web Development

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