Admin Moderation (/admin/moderation): - Dashboard: pending shipper/driver verifications, payouts, disputes - Approve/reject shipper registrations - Approve/reject driver registrations - Process payouts (approve/reject) - Resolve disputes (refund shipper or release to driver) - Stats overview (total shippers, drivers, loads, disputes) Added Moderation link to admin sidebar
200 lines
7.9 KiB
Text
200 lines
7.9 KiB
Text
<%- include('../partials/header', { activeMenu: 'moderation' }) %>
|
|
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">🔒 Admin Moderation</h1>
|
|
<p class="page-subtitle">Verify users, process payouts, resolve disputes</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-grid mb-4">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">👤</div>
|
|
<div class="stat-value"><%= stats.totalShippers || 0 %></div>
|
|
<div class="stat-label">Shippers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🚚</div>
|
|
<div class="stat-value"><%= stats.totalDrivers || 0 %></div>
|
|
<div class="stat-label">Drivers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon">📑</div>
|
|
<div class="stat-value"><%= stats.totalLoads || 0 %></div>
|
|
<div class="stat-label">Loads</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon">⚠</div>
|
|
<div class="stat-value" style="color:#dc3545;"><%= stats.openDisputes || 0 %></div>
|
|
<div class="stat-label">Disputes</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<!-- Pending Shipper Verifications -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🏢 Pending Shipper Verifications (<%= pendingShippers.length %>)</h3>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<% if (pendingShippers.length === 0) { %>
|
|
<div class="empty-state" style="padding:24px;"><p>No pending verifications</p></div>
|
|
<% } else { %>
|
|
<table class="table">
|
|
<thead><tr><th>Name</th><th>Phone</th><th>City</th><th></th></tr></thead>
|
|
<tbody>
|
|
<% for (const s of pendingShippers) { %>
|
|
<tr>
|
|
<td>
|
|
<strong><%= s.name %></strong>
|
|
<% if (s.company_name) { %><br><small style="color:#666;"><%= s.company_name %></small><% } %>
|
|
</td>
|
|
<td><%= s.phone %></td>
|
|
<td><%= s.city || 'N/A' %></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-success" onclick="approveShipper('<%= s.id %>')">✔</button>
|
|
<button class="btn btn-sm btn-danger" onclick="rejectShipper('<%= s.id %>')">✖</button>
|
|
</td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pending Driver Verifications -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🚚 Pending Driver Verifications (<%= pendingDrivers.length %>)</h3>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<% if (pendingDrivers.length === 0) { %>
|
|
<div class="empty-state" style="padding:24px;"><p>No pending verifications</p></div>
|
|
<% } else { %>
|
|
<table class="table">
|
|
<thead><tr><th>Driver</th><th>Vehicle</th><th>Type</th><th></th></tr></thead>
|
|
<tbody>
|
|
<% for (const d of pendingDrivers) { %>
|
|
<tr>
|
|
<td>
|
|
<strong><%= d.driver_name || 'N/A' %></strong>
|
|
<br><small style="color:#666;"><%= d.phone || '' %></small>
|
|
</td>
|
|
<td><%= d.number %></td>
|
|
<td><span class="badge badge-gray"><%= d.vehicle_type || 'N/A' %></span></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-success" onclick="approveDriver('<%= d.id %>')">✔</button>
|
|
<button class="btn btn-sm btn-danger" onclick="rejectDriver('<%= d.id %>')">✖</button>
|
|
</td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pending Payouts -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">💰 Pending Payouts (<%= pendingPayouts.length %>)</h3>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<% if (pendingPayouts.length === 0) { %>
|
|
<div class="empty-state" style="padding:24px;"><p>No pending payouts</p></div>
|
|
<% } else { %>
|
|
<table class="table">
|
|
<thead><tr><th>Driver</th><th>Amount</th><th>Method</th><th></th></tr></thead>
|
|
<tbody>
|
|
<% for (const p of pendingPayouts) { %>
|
|
<tr>
|
|
<td>
|
|
<strong><%= p.vehicles?.driver_name || 'N/A' %></strong>
|
|
<br><small style="color:#666;"><%= p.vehicles?.number || '' %></small>
|
|
</td>
|
|
<td style="font-weight:700;">₹ <%= (p.amount / 100).toLocaleString('en-IN') %></td>
|
|
<td><%= p.upi_id ? 'UPI' : 'Bank' %></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-success" onclick="processPayout('<%= p.id %>', 'approve')">✔ Process</button>
|
|
<button class="btn btn-sm btn-danger" onclick="processPayout('<%= p.id %>', 'reject')">✖</button>
|
|
</td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Open Disputes -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">⚠ Open Disputes (<%= openDisputes.length %>)</h3>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<% if (openDisputes.length === 0) { %>
|
|
<div class="empty-state" style="padding:24px;"><p>No open disputes</p></div>
|
|
<% } else { %>
|
|
<table class="table">
|
|
<thead><tr><th>Load</th><th>Reason</th><th>Amount</th><th></th></tr></thead>
|
|
<tbody>
|
|
<% for (const d of openDisputes) { %>
|
|
<tr>
|
|
<td>
|
|
<%= d.loads?.from_city || '?' %> → <%= d.loads?.to_city || '?' %>
|
|
<br><small style="color:#666;"><%= new Date(d.created_at).toLocaleDateString('en-IN') %></small>
|
|
</td>
|
|
<td style="max-width:200px;font-size:13px;"><%= d.reason %></td>
|
|
<td style="font-weight:700;">₹ <%= (d.loads?.driver_freight || 0).toLocaleString('en-IN') %></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-primary" onclick="resolveDispute('<%= d.id %>')">Resolve</button>
|
|
</td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function approveShipper(id) {
|
|
await fetch('/admin/moderation/shippers/' + id + '/approve', { method: 'POST' });
|
|
location.reload();
|
|
}
|
|
async function rejectShipper(id) {
|
|
if (!confirm('Reject this shipper?')) return;
|
|
await fetch('/admin/moderation/shippers/' + id + '/reject', { method: 'POST', body: JSON.stringify({ reason: 'Rejected' }), headers: { 'Content-Type': 'application/json' } });
|
|
location.reload();
|
|
}
|
|
async function approveDriver(id) {
|
|
await fetch('/admin/moderation/drivers/' + id + '/approve', { method: 'POST' });
|
|
location.reload();
|
|
}
|
|
async function rejectDriver(id) {
|
|
if (!confirm('Reject this driver?')) return;
|
|
await fetch('/admin/moderation/drivers/' + id + '/reject', { method: 'POST', body: JSON.stringify({ reason: 'Rejected' }), headers: { 'Content-Type': 'application/json' } });
|
|
location.reload();
|
|
}
|
|
async function processPayout(id, action) {
|
|
if (!confirm(action === 'approve' ? 'Approve and process payout?' : 'Reject payout request?')) return;
|
|
await fetch('/admin/moderation/payouts/' + id + '/process', { method: 'POST', body: JSON.stringify({ action }), headers: { 'Content-Type': 'application/json' } });
|
|
location.reload();
|
|
}
|
|
async function resolveDispute(id) {
|
|
const action = confirm('Click OK to release funds to DRIVER, Cancel to REFUND SHIPPER.');
|
|
const resolution = prompt('Resolution notes:');
|
|
if (!resolution) return;
|
|
await fetch('/admin/moderation/disputes/' + id + '/resolve', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ resolution, action: action ? 'release_driver' : 'refund_shipper' }),
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
location.reload();
|
|
}
|
|
</script>
|
|
|
|
<%- include('../partials/footer') %>
|