PHP WebShell
Текущая директория: /var/www/bitcardoApp/user/giftcards
Просмотр файла: card_transactions.php
<?php
// users/giftcards/card_transactions.php
include '../common/header.php';
require_once '../../config/db_config.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'Please sign in to view your card transactions.';
header("Location: ../login.php");
exit;
}
$user_id = (int)$_SESSION['user_id'];
// Base URL for card evidence images (same as admin)
$IMG_BASE = 'https://wallet.bitcardo.com/backyard/uploads/cards/';
// Pagination (by group/batch_ref)
$per_page = 10;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $per_page;
// Count total groups
$count_stmt = $conn->prepare("
SELECT COUNT(*) AS total
FROM (
SELECT batch_ref
FROM card_trade
WHERE user_id = ?
GROUP BY batch_ref
) t
");
$count_stmt->bind_param("i", $user_id);
$count_stmt->execute();
$total_groups = (int)($count_stmt->get_result()->fetch_assoc()['total'] ?? 0);
$count_stmt->close();
$total_pages = max(1, (int)ceil($total_groups / $per_page));
// Fetch paginated groups summary
$group_stmt = $conn->prepare("
SELECT batch_ref,
MIN(trade_created) AS created_at,
COUNT(*) AS items_count,
COALESCE(SUM(est_payout_ngn), 0) AS est_total,
GROUP_CONCAT(DISTINCT trade_status) AS status_concat
FROM card_trade
WHERE user_id = ?
GROUP BY batch_ref
ORDER BY created_at DESC
LIMIT ? OFFSET ?
");
$group_stmt->bind_param("iii", $user_id, $per_page, $offset);
$group_stmt->execute();
$groups_res = $group_stmt->get_result();
function badgeForStatus($status) {
$status = strtolower((string)$status);
return match ($status) {
'pending' => '<span class="badge bg-warning text-dark">Pending</span>',
'processing' => '<span class="badge bg-info text-dark">Processing</span>',
'approved', 'completed', 'paid'
=> '<span class="badge bg-success">Completed</span>',
'rejected', 'failed', 'declined'
=> '<span class="badge bg-danger">Rejected</span>',
'partial' => '<span class="badge bg-secondary">Partial</span>',
'review', 'on_hold'
=> '<span class="badge bg-secondary">On Review</span>',
default => '<span class="badge bg-light text-dark">'.htmlspecialchars(ucfirst($status)).'</span>',
};
}
function deriveGroupStatus($status_concat) {
if (!$status_concat) return 'pending';
$statuses = array_map('strtolower', array_filter(array_map('trim', explode(',', $status_concat))));
$unique = array_unique($statuses);
$allCompleted = !array_diff($unique, ['approved','completed','paid']);
$allRejected = !array_diff($unique, ['rejected','failed','declined']);
if ($allCompleted) return 'completed';
if ($allRejected) return 'rejected';
if (in_array('rejected', $unique, true) || in_array('failed', $unique, true) || in_array('declined', $unique, true)) {
return 'partial';
}
if (in_array('processing', $unique, true)) return 'processing';
if (in_array('pending', $unique, true)) return 'pending';
return 'partial';
}
?>
<div class="container mt-4">
<div class="row">
<?php include '../common/nav.php'; ?>
<main class="col-md-9 col-lg-10 px-md-5 mb-5">
<?php include '../common/page-header.php'; ?>
<div class="container my-5 px-md-5">
<div class="d-flex align-items-center justify-content-between mb-3">
<h5 class="mb-0 ps-3">Card Trades</h5>
<a href="submit_card.php" class="btn btn-primary btn-sm pe-3">
<i class="bi bi-plus-circle"></i> Trade New Cards
</a>
</div>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger"><?= $_SESSION['error']; unset($_SESSION['error']); ?></div>
<?php elseif (isset($_SESSION['success'])): ?>
<div class="alert alert-success"><?= $_SESSION['success']; unset($_SESSION['success']); ?></div>
<?php endif; ?>
<?php if ($total_groups === 0): ?>
<div class="alert alert-info">No card transactions yet.</div>
<?php else: ?>
<div class="accordion" id="txAccordion">
<?php
$idx = 0;
while ($g = $groups_res->fetch_assoc()):
$idx++;
$batch_ref = $g['batch_ref'];
$created_at = $g['created_at'];
$items_count = (int)$g['items_count'];
$est_total = (float)$g['est_total'];
$group_status = deriveGroupStatus($g['status_concat']);
// Fetch items for this batch
$items_stmt = $conn->prepare("
SELECT ct.trade_id, ct.card_ref, ct.cbrand_id, ct.gc_id, ct.card_value, ct.card_curr,
ct.buy_price_snapshot, ct.est_payout_ngn, ct.trade_status, ct.trade_created,
cb.card_brand, gc.demon,
(SELECT COUNT(*) FROM card_trade_images i WHERE i.trade_id = ct.trade_id) AS img_count
FROM card_trade ct
JOIN card_brands cb ON cb.cbrand_id = ct.cbrand_id
JOIN gift_cards gc ON gc.gc_id = ct.gc_id
WHERE ct.user_id = ? AND ct.batch_ref = ?
ORDER BY ct.trade_created ASC, ct.trade_id ASC
");
$items_stmt->bind_param("is", $user_id, $batch_ref);
$items_stmt->execute();
$items = $items_stmt->get_result();
?>
<div class="accordion-item mb-2 rounded-3 overflow-hidden">
<h2 class="accordion-header" id="heading<?= $idx ?>">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse<?= $idx ?>" aria-expanded="false" aria-controls="collapse<?= $idx ?>">
<div class="w-100 d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">Batch: <?= htmlspecialchars($batch_ref) ?></div>
<small class="text-muted"><?= htmlspecialchars(date('M j, Y g:i A', strtotime($created_at))) ?></small>
</div>
<div class="text-end pe-2">
<div><?= badgeForStatus($group_status) ?></div>
<small class="text-muted">
<?= $items_count ?> card<?= $items_count>1?'s':'' ?> • Est: ₦<?= number_format($est_total, 2) ?>
</small>
</div>
</div>
</button>
</h2>
<div id="collapse<?= $idx ?>" class="accordion-collapse collapse" aria-labelledby="heading<?= $idx ?>" data-bs-parent="#txAccordion">
<div class="accordion-body">
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th class="text-muted small">#</th>
<th class="d-none d-md-table-cell">Gift Card</th>
<!-- Desktop-only column headers -->
<th class="text-end d-none d-md-table-cell">Value</th>
<th class="text-end d-none d-md-table-cell">Price (₦)</th>
<th class="text-end d-none d-md-table-cell">Est. (₦)</th>
<th class="d-none d-md-table-cell">Status</th>
<th class="d-none d-md-table-cell">Created</th>
</tr>
</thead>
<tbody>
<tbody>
<?php
$n = 0;
while ($r = $items->fetch_assoc()):
$n++;
// Determine status and prepare evidence image (only for declined/failed/rejected)
$statusLower = strtolower((string)$r['trade_status']);
$evidenceImgUrl = null;
if ((int)$r['img_count'] > 0 && in_array($statusLower, ['declined','failed','rejected'], true)) {
$img_stmt = $conn->prepare("
SELECT path
FROM card_trade_images
WHERE trade_id = ?
ORDER BY image_id DESC
LIMIT 1
");
$img_stmt->bind_param("i", $r['trade_id']);
$img_stmt->execute();
$img_stmt->bind_result($imgPath);
if ($img_stmt->fetch() && $imgPath) {
// Build full URL; if path already absolute, keep it
$evidenceImgUrl = preg_match('#^https?://#i', $imgPath)
? $imgPath
: $IMG_BASE . ltrim($imgPath, '/');
}
$img_stmt->close();
}
?>
<tr>
<!-- # -->
<td class="text-muted small text-nowrap"><?= $n ?></td>
<!-- GiftCard + Mobile details stack -->
<td>
<div class="fw-semibold mb-0"><?= htmlspecialchars($r['card_brand']) ?></div>
<small class="text-muted d-block"><?= htmlspecialchars($r['demon']) ?></small>
<small class="text-muted d-block">Ref: <?= htmlspecialchars($r['card_ref']) ?></small>
<!-- MOBILE-ONLY: stacked details (labels + values) -->
<div class="d-md-none mt-2 border-top pt-2">
<div class="d-flex justify-content-between">
<small class="text-muted">Value</small>
<span class="text-nowrap">
<?= number_format((float)$r['card_value'], 2) ?>
<small class="text-muted"><?= htmlspecialchars($r['card_curr']) ?></small>
</span>
</div>
<div class="d-flex justify-content-between">
<small class="text-muted">Price</small>
<span class="text-nowrap">₦<?= number_format((float)$r['buy_price_snapshot'], 2) ?></span>
</div>
<div class="d-flex justify-content-between">
<small class="text-muted">Est.</small>
<span class="fw-semibold text-nowrap">₦<?= number_format((float)$r['est_payout_ngn'], 2) ?></span>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Status</small>
<span><?= badgeForStatus($r['trade_status']) ?></span>
</div>
<div class="d-flex justify-content-between">
<small class="text-muted">Created</small>
<small class="text-muted text-nowrap"><?= htmlspecialchars(date('M j, Y g:i A', strtotime($r['trade_created']))) ?></small>
</div>
</div>
<?php if (!empty($evidenceImgUrl)): ?>
<div class="mt-2">
<small class="text-muted d-block mb-1">Decline evidence</small>
<button
type="button"
class="btn btn-link p-0 border-0 card-evidence-btn"
data-bs-toggle="modal"
data-bs-target="#evidenceModal"
data-img="<?= htmlspecialchars($evidenceImgUrl) ?>"
>
<img
src="<?= htmlspecialchars($evidenceImgUrl) ?>"
alt="Decline evidence"
class="img-thumbnail"
style="max-height: 120px;"
>
</button>
</div>
<?php endif; ?>
</td>
<!-- DESKTOP-ONLY cells -->
<td class="text-end d-none d-md-table-cell text-nowrap">
<?= number_format((float)$r['card_value'], 2) ?>
<small class="text-muted"><?= htmlspecialchars($r['card_curr']) ?></small>
</td>
<td class="text-end d-none d-md-table-cell text-nowrap">₦<?= number_format((float)$r['buy_price_snapshot'], 2) ?></td>
<td class="text-end d-none d-md-table-cell fw-semibold text-nowrap">₦<?= number_format((float)$r['est_payout_ngn'], 2) ?></td>
<td class="d-none d-md-table-cell"><?= badgeForStatus($r['trade_status']) ?></td>
<td class="d-none d-md-table-cell"><small class="text-muted"><?= htmlspecialchars(date('M j, Y g:i A', strtotime($r['trade_created']))) ?></small></td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
$items_stmt->close();
endwhile;
$group_stmt->close();
?>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<nav class="mt-3">
<ul class="pagination">
<?php
$base = 'card_transactions.php';
$q = $_GET; unset($q['page']);
$qs = http_build_query($q);
$url = fn($p) => $base . '?' . ($qs ? $qs.'&' : '') . 'page=' . $p;
?>
<li class="page-item <?= $page<=1?'disabled':'' ?>">
<a class="page-link" href="<?= $page<=1?'#':htmlspecialchars($url($page-1)) ?>">Previous</a>
</li>
<?php
$start = max(1, $page-2);
$end = min($total_pages, $page+2);
if ($start > 1) {
echo '<li class="page-item"><a class="page-link" href="'.htmlspecialchars($url(1)).'">1</a></li>';
if ($start > 2) echo '<li class="page-item disabled"><span class="page-link">…</span></li>';
}
for ($p=$start; $p<=$end; $p++) {
echo '<li class="page-item '.($p==$page?'active':'').'"><a class="page-link" href="'.htmlspecialchars($url($p)).'">'.$p.'</a></li>';
}
if ($end < $total_pages) {
if ($end < $total_pages-1) echo '<li class="page-item disabled"><span class="page-link">…</span></li>';
echo '<li class="page-item"><a class="page-link" href="'.htmlspecialchars($url($total_pages)).'">'.$total_pages.'</a></li>';
}
?>
<li class="page-item <?= $page>=$total_pages?'disabled':'' ?>">
<a class="page-link" href="<?= $page>=$total_pages?'#':htmlspecialchars($url($page+1)) ?>">Next</a>
</li>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>
</div>
</main>
</div>
</div>
<!-- Evidence Image Modal -->
<div class="modal fade" id="evidenceModal" tabindex="-1" aria-labelledby="evidenceModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title" id="evidenceModalLabel">Decline evidence</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="ratio ratio-16x9">
<img id="evidenceModalImg" src="" alt="Evidence" class="w-100 h-100" style="object-fit:contain;">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// When a decline evidence thumbnail is clicked, set image in modal
document.addEventListener('click', function (e) {
const btn = e.target.closest('.card-evidence-btn');
if (!btn) return;
const imgUrl = btn.getAttribute('data-img') || '';
const imgEl = document.getElementById('evidenceModalImg');
if (imgEl) {
imgEl.src = imgUrl;
}
});
</script>
<?php include '../common/footer.php'; ?>
Выполнить команду
Для локальной разработки. Не используйте в интернете!