PHP WebShell
Текущая директория: /var/www/bitcardoApp/backyard/user/giftcards
Просмотр файла: catalog.php
<?php
// backyard/user/giftcards/catalog.php
include '../common/header.php';
if (!isset($conn)) {
include_once '../../config/db_config.php';
}
require_once '../../models/giftcards/catalog.php';
date_default_timezone_set('Africa/Lagos');
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
// Inputs
$brand_id = isset($_GET['brand']) ? (int)$_GET['brand'] : 0;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 25;
// Data
$brands = gc_brands_all($conn);
$cards = gc_cards_by_brand($conn, $brand_id ?: null, $page, $per_page);
// Keep qs
function qurl(array $add=[]){
$qs = array_merge($_GET, $add);
return '?' . http_build_query($qs);
}
?>
<style>
/* Mobile-first layout */
.brand-chip{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid #eee;border-radius:.5rem;background:#fff;}
.brand-chip.active{border-color:#0d6efd;background:#eef5ff;}
.badge-dot{display:inline-block;width:.5rem;height:.5rem;border-radius:50%;margin-right:.25rem;}
.badge-dot.on{background:#28a745;}
.badge-dot.off{background:#dc3545;}
.table td, .table th{vertical-align:middle;}
</style>
<div class="nk-content nk-content-fluid mt-5">
<div class="container-xl wide-lg">
<div class="nk-content-body">
<div class="nk-block-head">
<div class="nk-block-between-md g-3 align-items-center">
<div class="nk-block-head-content">
<h4 class="nk-block-title fw-normal mb-1">Gift Card Catalog & Brand Manager</h4>
<p class="text-soft mb-0">Manage brands and their denominations (buy/sell prices & status).</p>
</div>
<div class="nk-block-head-content d-flex gap-2">
<button class="btn btn-primary btn-sm p-2" id="btnAddBrand"><i class="fa fa-plus"></i> Add Brand</button>
<button class="btn btn-outline-primary btn-sm p-2" id="btnAddCard" <?= $brand_id? '' : 'disabled'; ?>><i class="fa fa-plus"></i> Add Denomination</button>
</div>
</div>
</div>
<div class="nk-block">
<div class="row g-2">
<!-- Brands column -->
<div class="col-12 col-md-4">
<div class="card card-bordered">
<div class="card-inner">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Brands</h6>
<span class="small text-soft"><?= count($brands); ?> total</span>
</div>
<?php if (empty($brands)): ?>
<div class="alert alert-light border mb-0">No brands yet.</div>
<?php else: ?>
<div class="gy-2">
<?php foreach ($brands as $b):
$active = ($brand_id === (int)$b['cbrand_id']);
$statusOn = ((int)$b['status'] === 1);
?>
<div class="d-flex justify-content-between align-items-center mb-2 brand-chip <?= $active?'active':''; ?>">
<a href="<?= qurl(['brand'=>$b['cbrand_id'], 'page'=>1]); ?>" class="text-decoration-none text-dark d-flex align-items-center">
<span class="badge-dot <?= $statusOn?'on':'off'; ?>"></span>
<strong><?= h($b['card_brand']); ?></strong>
</a>
<div class="d-flex align-items-center gap-2">
<div class="form-check form-switch m-0">
<input class="form-check-input brandToggle" type="checkbox"
data-id="<?= (int)$b['cbrand_id']; ?>" <?= $statusOn?'checked':''; ?>>
</div>
<button class="btn btn-xs btn-outline-secondary editBrandBtn"
data-id="<?= (int)$b['cbrand_id']; ?>"
data-name="<?= h($b['card_brand']); ?>"
data-icon="<?= h($b['brand_icon'] ?? ''); ?>">
Edit
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Denoms column -->
<div class="col-12 col-md-8">
<div class="card card-bordered">
<div class="card-inner">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Denominations <?= $brand_id? 'for selected brand' : '(select a brand)'; ?></h6>
<?php if ($brand_id): ?>
<div class="small text-soft">Page <?= $cards['page']; ?> / <?= $cards['pages']; ?> • <?= number_format($cards['total']); ?> total</div>
<?php endif; ?>
</div>
<?php if (!$brand_id): ?>
<div class="alert alert-info mb-0">Select a brand on the left to view and manage denominations.</div>
<?php else: ?>
<?php if (empty($cards['rows'])): ?>
<div class="alert alert-light border mb-0">No denominations yet for this brand.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped">
<thead class="small text-soft">
<tr>
<th>Denom</th>
<th>Curr</th>
<th class="text-end">Buy Price</th>
<th class="text-end">Sell Price</th>
<th>Status</th>
<th class="text-end">Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($cards['rows'] as $r):
$on = ((int)$r['status'] === 1);
?>
<tr data-id="<?= (int)$r['gc_id']; ?>">
<td class="fw-semibold"><?= h($r['demon']); ?></td>
<td><?= h($r['card_curr']); ?></td>
<td class="text-end"><?= number_format((float)$r['buy_price'],2); ?></td>
<td class="text-end"><?= number_format((float)$r['sell_price'],2); ?></td>
<td><span class="badge <?= $on?'bg-success':'bg-danger'; ?>"><?= $on?'Active':'Inactive'; ?></span></td>
<td class="text-end small"><?= h($r['updated_at']); ?></td>
<td class="small">
<button class="btn btn-xs btn-outline-secondary editCardBtn"
data-id="<?= (int)$r['gc_id']; ?>"
data-brand="<?= (int)$r['cbrand_id']; ?>"
data-demon="<?= h($r['demon']); ?>"
data-curr="<?= h($r['card_curr']); ?>"
data-buy="<?= h($r['buy_price']); ?>"
data-sell="<?= h($r['sell_price']); ?>"
data-status="<?= (int)$r['status']; ?>">
Edit
</button>
<div class="form-check form-switch d-inline-block align-middle ms-2">
<input class="form-check-input cardToggle" type="checkbox" data-id="<?= (int)$r['gc_id']; ?>" <?= $on?'checked':''; ?>>
</div>
<button class="btn btn-xs btn-outline-danger ms-1 delCardBtn" data-id="<?= (int)$r['gc_id']; ?>">Del</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Pagination (simple) -->
<?php if ($cards['pages'] > 1):
$p = $cards['page']; $pages=$cards['pages']; $prev=max(1,$p-1); $next=min($pages,$p+1);
?>
<div class="d-flex justify-content-between align-items-center mt-2">
<div class="small text-soft">Page <?= $p; ?> of <?= $pages; ?></div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-secondary <?= $p<=1?'disabled':''; ?>" href="<?= qurl(['brand'=>$brand_id,'page'=>$prev]); ?>">‹ Prev</a>
<a class="btn btn-sm btn-outline-secondary <?= $p>=$pages?'disabled':''; ?>" href="<?= qurl(['brand'=>$brand_id,'page'=>$next]); ?>">Next ›</a>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
</div><!-- row -->
</div>
</div>
</div>
</div>
<!-- Brand Modal -->
<div class="modal fade" id="brandModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="brandForm">
<div class="modal-header">
<h6 class="modal-title" id="brandModalTitle">Add Brand</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="cbrand_id" id="b_id" value="">
<div class="mb-2">
<label class="form-label small">Brand Name</label>
<input type="text" class="form-control" name="card_brand" id="b_name" required>
</div>
<div>
<label class="form-label small">Icon (optional URL / filename)</label>
<input type="text" class="form-control" name="brand_icon" id="b_icon" placeholder="/assets/icons/apple.png">
</div>
<div class="small text-soft mt-2">Tip: You can store icons anywhere public; this field is just a reference.</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="brandSaveBtn" type="submit">Save</button>
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
<!-- Card Modal -->
<div class="modal fade" id="cardModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="cardForm">
<div class="modal-header">
<h6 class="modal-title" id="cardModalTitle">Add Denomination</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="gc_id" id="c_id" value="">
<div class="mb-2">
<label class="form-label small">Brand</label>
<select class="form-select" name="cbrand_id" id="c_brand" required>
<option value="">Select brand</option>
<?php foreach ($brands as $b): ?>
<option value="<?= (int)$b['cbrand_id']; ?>" <?= ($brand_id===(int)$b['cbrand_id'])?'selected':''; ?>>
<?= h($b['card_brand']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="mb-2 col-2">
<label class="form-label small">Curr.</label>
<input type="text" class="form-control" name="card_curr" id="c_curr" maxlength="1" placeholder="$" required>
</div>
<div class="mb-2 col-10">
<label class="form-label small">Denomination (e.g. USA 50 Cash Receipt)</label>
<input type="text" class="form-control" name="demon" id="c_demon" required>
</div>
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label small">Buy Price (₦)</label>
<input type="number" step="0.01" class="form-control" name="buy_price" id="c_buy">
</div>
<div class="col-6">
<label class="form-label small">Sell Price (₦)</label>
<input type="number" step="0.01" class="form-control" name="sell_price" id="c_sell" disabled>
</div>
</div>
<div class="mt-2 form-check form-switch">
<input class="form-check-input" type="checkbox" id="c_status" checked>
<label class="form-check-label small" for="c_status">Active</label>
</div>
<div class="small text-soft mt-2">Note: `card_curr` is limited to 1 character by your schema; use `$` for USD, etc.</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="cardSaveBtn" type="submit">Save</button>
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
<script>
(function(){
const postUrl = '../../models/giftcards/catalog_post.php';
// ---------- Brands ----------
const brandModal = document.getElementById('brandModal');
const brandForm = document.getElementById('brandForm');
const btnAddBrand= document.getElementById('btnAddBrand');
const brandTitle = document.getElementById('brandModalTitle');
const b_id = document.getElementById('b_id');
const b_name = document.getElementById('b_name');
const b_icon = document.getElementById('b_icon');
btnAddBrand.addEventListener('click', ()=>{
b_id.value = '';
b_name.value = '';
b_icon.value = '';
brandTitle.textContent = 'Add Brand';
bootstrap.Modal.getOrCreateInstance(brandModal).show();
});
document.querySelectorAll('.editBrandBtn').forEach(btn=>{
btn.addEventListener('click', ()=>{
b_id.value = btn.getAttribute('data-id') || '';
b_name.value = btn.getAttribute('data-name') || '';
b_icon.value = btn.getAttribute('data-icon') || '';
brandTitle.textContent = 'Edit Brand';
bootstrap.Modal.getOrCreateInstance(brandModal).show();
});
});
brandForm.addEventListener('submit', async function(e){
e.preventDefault();
const id = b_id.value.trim();
const payload = {
action: id? 'brand.update' : 'brand.create',
cbrand_id: id ? Number(id) : undefined,
card_brand: b_name.value.trim(),
brand_icon: b_icon.value.trim() || null
};
const r = await fetch(postUrl, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
const t = await r.text(); let data;
try{ data = JSON.parse(t); }catch{ alert('Bad response: '+t.substring(0,200)); return; }
if (!data.success){ alert(data.message || 'Failed to save brand'); return; }
location.href = location.pathname + location.search; // keep current selection
});
document.querySelectorAll('.brandToggle').forEach(ch=>{
ch.addEventListener('change', async ()=>{
const id = Number(ch.getAttribute('data-id'));
const status = ch.checked ? 1 : 0;
const r = await fetch(postUrl,{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'brand.toggle', cbrand_id:id, status})});
const t = await r.text(); let data;
try{ data = JSON.parse(t); }catch{ alert('Toggle failed: '+t.substring(0,200)); return; }
if(!data.success){ alert('Failed to toggle brand'); ch.checked = !ch.checked; return; }
// stay
location.href = location.pathname + location.search;
});
});
// ---------- Cards ----------
const cardModal = document.getElementById('cardModal');
const cardForm = document.getElementById('cardForm');
const btnAddCard= document.getElementById('btnAddCard');
const cardTitle = document.getElementById('cardModalTitle');
const c_id = document.getElementById('c_id');
const c_brand= document.getElementById('c_brand');
const c_demon= document.getElementById('c_demon');
const c_curr = document.getElementById('c_curr');
const c_buy = document.getElementById('c_buy');
const c_sell = document.getElementById('c_sell');
const c_status = document.getElementById('c_status');
btnAddCard.addEventListener('click', ()=>{
c_id.value=''; c_demon.value=''; c_curr.value='$'; c_buy.value=''; c_sell.value=''; c_status.checked=true;
cardTitle.textContent='Add Denomination';
bootstrap.Modal.getOrCreateInstance(cardModal).show();
});
document.querySelectorAll('.editCardBtn').forEach(btn=>{
btn.addEventListener('click', ()=>{
c_id.value = btn.getAttribute('data-id') || '';
c_brand.value = btn.getAttribute('data-brand') || '';
c_demon.value = btn.getAttribute('data-demon') || '';
c_curr.value = btn.getAttribute('data-curr') || '$';
c_buy.value = btn.getAttribute('data-buy') || '';
c_sell.value = btn.getAttribute('data-sell') || '';
c_status.checked = (btn.getAttribute('data-status')==='1');
cardTitle.textContent='Edit Denomination';
bootstrap.Modal.getOrCreateInstance(cardModal).show();
});
});
cardForm.addEventListener('submit', async function(e){
e.preventDefault();
const id = c_id.value.trim();
const payload = {
action: id ? 'card.update' : 'card.create',
gc_id: id ? Number(id) : undefined,
cbrand_id: Number(c_brand.value),
demon: c_demon.value.trim(),
card_curr: c_curr.value.trim(),
buy_price: c_buy.value===''? null : Number(c_buy.value),
sell_price: c_sell.value===''? null : Number(c_sell.value),
status: c_status.checked ? 1 : 0
};
const r = await fetch(postUrl,{method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
const t = await r.text(); let data;
try{ data = JSON.parse(t); }catch{ alert('Bad response: '+t.substring(0,200)); return; }
if(!data.success){ alert(data.message || 'Failed to save denomination'); return; }
// stay on brand list
const params = new URLSearchParams(window.location.search);
if (!params.has('brand')) params.set('brand', String(payload.cbrand_id));
window.location.href = location.pathname + '?' + params.toString();
});
document.querySelectorAll('.cardToggle').forEach(ch=>{
ch.addEventListener('change', async ()=>{
const id = Number(ch.getAttribute('data-id'));
const status = ch.checked ? 1 : 0;
const r = await fetch(postUrl,{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'card.toggle', gc_id:id, status})});
const t = await r.text(); let data;
try{ data = JSON.parse(t); }catch{ alert('Toggle failed: '+t.substring(0,200)); return; }
if(!data.success){ alert('Failed to toggle card'); ch.checked=!ch.checked; }
});
});
document.querySelectorAll('.delCardBtn').forEach(btn=>{
btn.addEventListener('click', async ()=>{
const id = Number(btn.getAttribute('data-id'));
if (!confirm('Delete this denomination?')) return;
const r = await fetch(postUrl,{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'card.delete', gc_id:id})});
const t = await r.text(); let data;
try{ data = JSON.parse(t); }catch{ alert('Delete failed: '+t.substring(0,200)); return; }
if(!data.success){ alert('Failed to delete card'); return; }
location.reload();
});
});
})();
</script>
<?php include '../common/footer.php'; ?>
Выполнить команду
Для локальной разработки. Не используйте в интернете!