[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
This commit is contained in:
parent
4923357e29
commit
6be15fb059
4 changed files with 454 additions and 0 deletions
249
webapp/src/routes/admin-moderation.js
Normal file
249
webapp/src/routes/admin-moderation.js
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const supabase = require('../services/supabase');
|
||||||
|
const { asyncHandler } = require('../middleware/security');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MIDDLEWARE — Admin only
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function requireAdmin(req, res, next) {
|
||||||
|
if (!req.session.userId) {
|
||||||
|
return res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MODERATION DASHBOARD
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.get('/', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
// Pending verifications
|
||||||
|
const { data: pendingShippers } = await supabase
|
||||||
|
.from('shippers')
|
||||||
|
.select('*')
|
||||||
|
.eq('is_verified', false)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
const { data: pendingDrivers } = await supabase
|
||||||
|
.from('vehicles')
|
||||||
|
.select('*')
|
||||||
|
.eq('is_verified', false)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
// Pending payouts
|
||||||
|
const { data: pendingPayouts } = await supabase
|
||||||
|
.from('payout_requests')
|
||||||
|
.select('*, vehicles(number, driver_name)')
|
||||||
|
.eq('status', 'pending')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
// Open disputes
|
||||||
|
const { data: openDisputes } = await supabase
|
||||||
|
.from('disputes')
|
||||||
|
.select('*, loads(from_city, to_city, driver_freight)')
|
||||||
|
.eq('status', 'open')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const { count: totalShippers } = await supabase.from('shippers').select('*', { count: 'exact', head: true });
|
||||||
|
const { count: totalDrivers } = await supabase.from('vehicles').select('*', { count: 'exact', head: true });
|
||||||
|
const { count: totalLoads } = await supabase.from('loads').select('*', { count: 'exact', head: true });
|
||||||
|
const { count: openDisputesCount } = await supabase.from('disputes').select('*', { count: 'exact', head: true }).eq('status', 'open');
|
||||||
|
|
||||||
|
res.render('pages/admin/moderation', {
|
||||||
|
pendingShippers: pendingShippers || [],
|
||||||
|
pendingDrivers: pendingDrivers || [],
|
||||||
|
pendingPayouts: pendingPayouts || [],
|
||||||
|
openDisputes: openDisputes || [],
|
||||||
|
stats: { totalShippers, totalDrivers, totalLoads, openDisputes: openDisputesCount },
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// APPROVE/REJECT SHIPPER
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.post('/shippers/:id/approve', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
await supabase.from('shippers').update({ is_verified: true }).eq('id', req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/shippers/:id/reject', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
const { reason } = req.body;
|
||||||
|
await supabase.from('shippers').update({ is_verified: false }).eq('id', req.params.id);
|
||||||
|
// TODO: notify shipper
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// APPROVE/REJECT DRIVER
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.post('/drivers/:id/approve', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
await supabase.from('vehicles').update({ is_verified: true }).eq('id', req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/drivers/:id/reject', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
await supabase.from('vehicles').update({ is_verified: false }).eq('id', req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// RESOLVE DISPUTE
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.post('/disputes/:id/resolve', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
const { resolution, action } = req.body; // action: 'refund_shipper' or 'release_driver'
|
||||||
|
|
||||||
|
const { data: dispute } = await supabase
|
||||||
|
.from('disputes')
|
||||||
|
.select('*, loads(*)')
|
||||||
|
.eq('id', req.params.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!dispute) return res.status(404).json({ error: 'Dispute not found' });
|
||||||
|
|
||||||
|
if (action === 'refund_shipper') {
|
||||||
|
// Refund to shipper
|
||||||
|
const shipperAccount = await supabase
|
||||||
|
.from('escrow_accounts')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', dispute.loads.shipper_id)
|
||||||
|
.eq('role', 'shipper')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (shipperAccount.data) {
|
||||||
|
await supabase.from('escrow_accounts').update({
|
||||||
|
balance: shipperAccount.data.balance + dispute.loads.escrow_amount,
|
||||||
|
held_balance: Math.max(0, shipperAccount.data.held_balance - dispute.loads.escrow_amount),
|
||||||
|
}).eq('id', shipperAccount.data.id);
|
||||||
|
|
||||||
|
await supabase.from('escrow_transactions').insert({
|
||||||
|
escrow_account_id: shipperAccount.data.id,
|
||||||
|
load_id: dispute.load_id,
|
||||||
|
type: 'refund',
|
||||||
|
amount: dispute.loads.escrow_amount,
|
||||||
|
status: 'completed',
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase.from('loads').update({ payment_status: 'refunded' }).eq('id', dispute.load_id);
|
||||||
|
} else if (action === 'release_driver') {
|
||||||
|
// Release to driver
|
||||||
|
const driverAccount = await supabase
|
||||||
|
.from('escrow_accounts')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', dispute.raised_against)
|
||||||
|
.eq('role', 'driver')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (driverAccount.data) {
|
||||||
|
await supabase.from('escrow_accounts').update({
|
||||||
|
balance: driverAccount.data.balance + dispute.loads.escrow_amount,
|
||||||
|
held_balance: Math.max(0, driverAccount.data.held_balance - dispute.loads.escrow_amount),
|
||||||
|
}).eq('id', driverAccount.data.id);
|
||||||
|
|
||||||
|
await supabase.from('escrow_transactions').insert({
|
||||||
|
escrow_account_id: driverAccount.data.id,
|
||||||
|
load_id: dispute.load_id,
|
||||||
|
type: 'release',
|
||||||
|
amount: dispute.loads.escrow_amount,
|
||||||
|
status: 'completed',
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase.from('loads').update({ payment_status: 'released', settled_at: new Date().toISOString() }).eq('id', dispute.load_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dispute
|
||||||
|
await supabase.from('disputes').update({
|
||||||
|
status: 'resolved',
|
||||||
|
resolution,
|
||||||
|
resolved_by: req.session.userId,
|
||||||
|
resolved_at: new Date().toISOString(),
|
||||||
|
}).eq('id', req.params.id);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PROCESS PAYOUT
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.post('/payouts/:id/process', requireAdmin, asyncHandler(async (req, res) => {
|
||||||
|
const { action } = req.body; // 'approve' or 'reject'
|
||||||
|
|
||||||
|
const { data: payout } = await supabase
|
||||||
|
.from('payout_requests')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', req.params.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!payout) return res.status(404).json({ error: 'Payout not found' });
|
||||||
|
|
||||||
|
if (action === 'approve') {
|
||||||
|
await supabase.from('payout_requests').update({
|
||||||
|
status: 'processed',
|
||||||
|
processed_by: req.session.userId,
|
||||||
|
processed_at: new Date().toISOString(),
|
||||||
|
}).eq('id', req.params.id);
|
||||||
|
|
||||||
|
// Deduct from held balance
|
||||||
|
const { data: account } = await supabase
|
||||||
|
.from('escrow_accounts')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', payout.user_id)
|
||||||
|
.eq('role', 'driver')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
await supabase.from('escrow_accounts').update({
|
||||||
|
held_balance: Math.max(0, account.held_balance - payout.amount),
|
||||||
|
total_withdrawn: account.total_withdrawn + payout.amount,
|
||||||
|
}).eq('id', account.id);
|
||||||
|
|
||||||
|
await supabase.from('escrow_transactions').insert({
|
||||||
|
escrow_account_id: account.id,
|
||||||
|
type: 'payout',
|
||||||
|
amount: payout.amount,
|
||||||
|
status: 'completed',
|
||||||
|
reference_id: 'PAYOUT-' + payout.id,
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reject — return funds to available balance
|
||||||
|
const { data: account } = await supabase
|
||||||
|
.from('escrow_accounts')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', payout.user_id)
|
||||||
|
.eq('role', 'driver')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
await supabase.from('escrow_accounts').update({
|
||||||
|
balance: account.balance + payout.amount,
|
||||||
|
held_balance: Math.max(0, account.held_balance - payout.amount),
|
||||||
|
}).eq('id', account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase.from('payout_requests').update({
|
||||||
|
status: 'rejected',
|
||||||
|
processed_by: req.session.userId,
|
||||||
|
processed_at: new Date().toISOString(),
|
||||||
|
}).eq('id', req.params.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -212,6 +212,7 @@ app.use('/portal-users', require('./routes/portal-users'));
|
||||||
app.use('/api', require('./routes/api'));
|
app.use('/api', require('./routes/api'));
|
||||||
app.use('/marketplace', require('./routes/marketplace'));
|
app.use('/marketplace', require('./routes/marketplace'));
|
||||||
app.use('/escrow', require('./routes/payments'));
|
app.use('/escrow', require('./routes/payments'));
|
||||||
|
app.use('/admin/moderation', require('./routes/admin-moderation'));
|
||||||
app.use('/', require('./routes/public'));
|
app.use('/', require('./routes/public'));
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
|
|
|
||||||
200
webapp/src/views/pages/admin/moderation.ejs
Normal file
200
webapp/src/views/pages/admin/moderation.ejs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
<%- 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') %>
|
||||||
|
|
@ -55,6 +55,10 @@
|
||||||
<a href="/invoices" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'invoices' ? 'active' : '' %>">📄 Invoices</a>
|
<a href="/invoices" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'invoices' ? 'active' : '' %>">📄 Invoices</a>
|
||||||
<a href="/audit-logs" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'audit' ? 'active' : '' %>">📜 Audit Logs</a>
|
<a href="/audit-logs" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'audit' ? 'active' : '' %>">📜 Audit Logs</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<span class="sidebar-title">Moderation</span>
|
||||||
|
<a href="/admin/moderation" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'moderation' ? 'active' : '' %>">🔒 Moderation</a>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue