freightdesk/webapp/src/views/pages/admin/moderation.ejs
FreightDesk 6be15fb059
Some checks are pending
FreightDesk CI/CD / Lint & Test (push) Waiting to run
FreightDesk CI/CD / Build Docker Image (push) Blocked by required conditions
FreightDesk CI/CD / Deploy to Coolify (push) Blocked by required conditions
[OWL] Admin moderation panel + deployment docs
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
2026-06-08 01:54:35 +00:00

200 lines
7.9 KiB
Text

<%- include('../partials/header', { activeMenu: 'moderation' }) %>
<div class="page-header">
<div>
<h1 class="page-title">&#128274; 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">&#128100;</div>
<div class="stat-value"><%= stats.totalShippers || 0 %></div>
<div class="stat-label">Shippers</div>
</div>
<div class="stat-card">
<div class="stat-icon">&#128666;</div>
<div class="stat-value"><%= stats.totalDrivers || 0 %></div>
<div class="stat-label">Drivers</div>
</div>
<div class="stat-card">
<div class="stat-icon">&#128209;</div>
<div class="stat-value"><%= stats.totalLoads || 0 %></div>
<div class="stat-label">Loads</div>
</div>
<div class="stat-card">
<div class="stat-icon">&#9888;</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">&#127970; 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 %>')">&#10004;</button>
<button class="btn btn-sm btn-danger" onclick="rejectShipper('<%= s.id %>')">&#10006;</button>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>
</div>
<!-- Pending Driver Verifications -->
<div class="card">
<div class="card-header">
<h3 class="card-title">&#128666; 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 %>')">&#10004;</button>
<button class="btn btn-sm btn-danger" onclick="rejectDriver('<%= d.id %>')">&#10006;</button>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>
</div>
<!-- Pending Payouts -->
<div class="card">
<div class="card-header">
<h3 class="card-title">&#128176; 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;">&#8377; <%= (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')">&#10004; Process</button>
<button class="btn btn-sm btn-danger" onclick="processPayout('<%= p.id %>', 'reject')">&#10006;</button>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>
</div>
<!-- Open Disputes -->
<div class="card">
<div class="card-header">
<h3 class="card-title">&#9888; 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 || '?' %> &rarr; <%= 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;">&#8377; <%= (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') %>