Admin Panel (Products & Orders)
A quick tour
The admin panel is a small website inside backend/admin/. The shop owner opens it in a browser to run the shop. These are real screenshots of the panel you are about to build:



Security basics of the panel
CSRF (Cross-Site Request Forgery): while the admin is logged in, a bad website could secretly submit a form to /admin/order.php. The hidden csrf field, checked with hash_equals(), makes such forged requests fail. XSS: a customer could type HTML into their name. Printing it with h() shows it as text:
<?php
$name = '<script>alert("hacked")</script>'; // a customer typed this as their name
echo $name; // ❌ the script runs in the admin's browser
echo h($name); // ✅ <script>… shown as plain textStep 1: inc/bootstrap.php
Every admin page starts with this file. It starts the session, and provides require_admin(), csrf_field() / check_csrf(), h(), flash messages and the safe image upload function.
<?php
// Shared by every admin page: session, login check, CSRF, escaping, uploads.
require_once __DIR__ . '/../../includes/db.php';
session_set_cookie_params(0, '/', '', isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', true);
session_name('KEADMIN');
session_start();
if (DEBUG) {
ini_set('display_errors', '1');
}
/** Escape text for HTML output. Use it for EVERY value printed in a page. */
function h($s)
{
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
}
function admin_url($page, $params = array())
{
return $page . (count($params) ? '?' . http_build_query($params) : '');
}
function redirect($url)
{
header('Location: ' . $url);
exit;
}
function require_admin()
{
if (empty($_SESSION['admin_id'])) {
redirect('login.php');
}
}
function csrf_token()
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(16));
}
return $_SESSION['csrf'];
}
function csrf_field()
{
return '<input type="hidden" name="csrf" value="' . h(csrf_token()) . '">';
}
/** Call at the top of every POST handler. */
function check_csrf()
{
$sent = isset($_POST['csrf']) ? $_POST['csrf'] : '';
if (!hash_equals(csrf_token(), $sent)) {
http_response_code(400);
exit('Invalid form token. Go back, refresh the page and try again.');
}
}
function flash($msg = null, $type = 'ok')
{
if ($msg !== null) {
$_SESSION['flash'] = array($msg, $type);
return null;
}
if (!empty($_SESSION['flash'])) {
$f = $_SESSION['flash'];
unset($_SESSION['flash']);
return $f;
}
return null;
}
function post($key, $default = '')
{
return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default;
}
function slugify($text)
{
$text = strtolower(trim(preg_replace('/[^A-Za-z0-9]+/', '-', $text), '-'));
return $text === '' ? 'item-' . time() : $text;
}
/**
* Save an uploaded image into uploads/<folder>/ and return its relative path,
* or null when no file was chosen. Only real JPG/PNG/WEBP images up to 2 MB.
*/
function save_upload($field, $folder)
{
if (empty($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
return null;
}
$f = $_FILES[$field];
if ($f['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed (error ' . $f['error'] . ').');
}
if ($f['size'] > 2 * 1024 * 1024) {
throw new RuntimeException('Image must be smaller than 2 MB.');
}
$info = @getimagesize($f['tmp_name']);
$types = array(IMAGETYPE_JPEG => 'jpg', IMAGETYPE_PNG => 'png', IMAGETYPE_WEBP => 'webp');
if ($info === false || !isset($types[$info[2]])) {
throw new RuntimeException('Only JPG, PNG or WEBP images are allowed.');
}
// Random file name: the user's file name is never used on disk.
$name = $folder . '-' . bin2hex(random_bytes(6)) . '.' . $types[$info[2]];
$dir = __DIR__ . '/../../uploads/' . $folder;
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
if (!move_uploaded_file($f['tmp_name'], $dir . '/' . $name)) {
throw new RuntimeException('Could not save the image.');
}
return 'uploads/' . $folder . '/' . $name;
}
function img_src($path)
{
if ($path === null || $path === '') {
return '';
}
return preg_match('#^https?://#', $path) ? $path : '../' . ltrim($path, '/');
}
function rupees($n)
{
return '₹' . number_format((float) $n, 2);
}
$ORDER_STATUSES = array('placed', 'confirmed', 'shipped', 'delivered', 'cancelled');Step 2: layout, menu and CSS
Each page sets $pageTitle and $active, includes the header (top bar + side menu + flash message), prints its content and includes the footer. The CSS is plain, with a mobile breakpoint at 800px.
<?php
// $pageTitle and $active are set by the page that includes this file.
$menu = array(
'index.php' => array('📊', 'Dashboard'),
'orders.php' => array('📦', 'Orders'),
'products.php' => array('🛍️', 'Products'),
'categories.php' => array('🗂️', 'Categories'),
'banners.php' => array('🖼️', 'Banners'),
'coupons.php' => array('🏷️', 'Coupons'),
'users.php' => array('👥', 'Customers'),
);
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><?= h($pageTitle) ?> · KartEasy Admin</title>
<link rel="stylesheet" href="assets/admin.css">
</head>
<body>
<header class="top">
<button class="menu-btn" onclick="document.body.classList.toggle('nav-open')" aria-label="Menu">☰</button>
<a class="brand" href="index.php">🛒 KartEasy <span>Admin</span></a>
<div class="who"><?= h(isset($_SESSION['admin_name']) ? $_SESSION['admin_name'] : '') ?> · <a href="logout.php">Logout</a></div>
</header>
<nav class="side">
<?php foreach ($menu as $file => $m): ?>
<a href="<?= h($file) ?>" class="<?= $active === $file ? 'on' : '' ?>"><span><?= $m[0] ?></span><?= h($m[1]) ?></a>
<?php endforeach; ?>
</nav>
<main class="main">
<h1><?= h($pageTitle) ?></h1>
<?php $f = flash(); if ($f): ?>
<div class="flash <?= h($f[1]) ?>"><?= h($f[0]) ?></div>
<?php endif; ?></main> </body> </html>
*{box-sizing:border-box}
:root{--brand:#4f46e5;--brand-d:#3730a3;--bg:#f4f5fb;--card:#fff;--text:#1f2937;--muted:#6b7280;--line:#e5e7eb;--ok:#059669;--err:#dc2626}
body{margin:0;font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text)}
a{color:var(--brand)}
.top{position:fixed;inset:0 0 auto 0;height:56px;background:var(--brand);color:#fff;display:flex;align-items:center;gap:12px;padding:0 16px;z-index:10}
.top .brand{color:#fff;text-decoration:none;font-weight:700;font-size:18px}
.top .brand span{font-weight:400;opacity:.8}
.top .who{margin-left:auto;font-size:14px}
.top .who a{color:#fff}
.menu-btn{display:none;background:none;border:0;color:#fff;font-size:22px;cursor:pointer}
.side{position:fixed;top:56px;bottom:0;left:0;width:220px;background:#fff;border-right:1px solid var(--line);padding:12px 8px}
.side a{display:flex;gap:10px;align-items:center;padding:10px 12px;border-radius:8px;color:var(--text);text-decoration:none;margin-bottom:2px}
.side a:hover{background:#eef2ff}
.side a.on{background:var(--brand);color:#fff}
.main{margin:56px 0 0 220px;padding:24px;max-width:1200px}
h1{margin:0 0 16px;font-size:24px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px}
.card .n{font-size:28px;font-weight:700}
.card .l{color:var(--muted);font-size:13px}
.box{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:20px;overflow-x:auto}
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:10px 8px;border-bottom:1px solid var(--line);vertical-align:middle}
th{font-size:12px;text-transform:uppercase;color:var(--muted);letter-spacing:.04em}
td img{width:44px;height:44px;object-fit:cover;border-radius:8px;background:#eee}
.btn{display:inline-block;background:var(--brand);color:#fff;border:0;border-radius:8px;padding:8px 14px;font-size:14px;cursor:pointer;text-decoration:none}
.btn:hover{background:var(--brand-d)}
.btn.sm{padding:5px 10px;font-size:13px}
.btn.gray{background:#e5e7eb;color:var(--text)}
.btn.red{background:var(--err)}
form.inline{display:inline}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
.toolbar input,.toolbar select{padding:8px;border:1px solid var(--line);border-radius:8px}
.form{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form .full{grid-column:1/-1}
.form label{display:block;font-size:13px;font-weight:600;margin-bottom:4px}
.form input,.form select,.form textarea{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:8px;font:inherit}
.form textarea{min-height:110px}
.check{display:flex;gap:8px;align-items:center}
.check input{width:auto}
.flash{padding:12px 14px;border-radius:8px;margin-bottom:16px;background:#ecfdf5;color:var(--ok);border:1px solid #a7f3d0}
.flash.err{background:#fef2f2;color:var(--err);border-color:#fecaca}
.badge{display:inline-block;padding:2px 10px;border-radius:99px;font-size:12px;font-weight:600;background:#eef2ff;color:var(--brand)}
.badge.placed{background:#fef3c7;color:#92400e}
.badge.confirmed{background:#dbeafe;color:#1e40af}
.badge.shipped{background:#e0e7ff;color:#3730a3}
.badge.delivered{background:#d1fae5;color:#065f46}
.badge.cancelled{background:#fee2e2;color:#991b1b}
.badge.off{background:#f3f4f6;color:var(--muted)}
.muted{color:var(--muted)}
.login{max-width:380px;margin:12vh auto;background:#fff;border:1px solid var(--line);border-radius:16px;padding:28px}
.login h1{text-align:center}
.login input{width:100%;padding:10px;border:1px solid var(--line);border-radius:8px;margin:6px 0 14px;font:inherit}
.login .btn{width:100%;padding:11px}
@media (max-width:800px){
.menu-btn{display:block}
.side{transform:translateX(-100%);transition:transform .2s;z-index:9}
body.nav-open .side{transform:none;box-shadow:0 0 40px rgba(0,0,0,.2)}
.main{margin-left:0;padding:16px}
.form{grid-template-columns:1fr}
.cards{grid-template-columns:1fr 1fr;gap:10px}
.card .n{font-size:20px}
.top .who{font-size:12px}
}Step 3: login and logout

After a correct password we call session_regenerate_id(true) — a new session id after login stops "session fixation" attacks. Failed attempts are counted in the session; after 5 in 10 minutes the form refuses.
<?php
require_once __DIR__ . '/inc/bootstrap.php';
if (!empty($_SESSION['admin_id'])) {
redirect('index.php');
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
// Slow down password guessing: max 5 failed tries per 10 minutes per session.
$tries = isset($_SESSION['login_tries']) ? $_SESSION['login_tries'] : array();
$tries = array_filter($tries, function ($t) {
return $t > time() - 600;
});
if (count($tries) >= 5) {
$error = 'Too many attempts. Please wait 10 minutes.';
} else {
$stmt = db()->prepare('SELECT id, name, password_hash FROM admins WHERE email = ?');
$stmt->execute(array(strtolower(post('email'))));
$admin = $stmt->fetch();
if ($admin && password_verify(post('password'), $admin['password_hash'])) {
session_regenerate_id(true); // new session id after login (stops session fixation)
$_SESSION['admin_id'] = (int) $admin['id'];
$_SESSION['admin_name'] = $admin['name'];
unset($_SESSION['login_tries']);
redirect('index.php');
}
$tries[] = time();
$error = 'Wrong email or password.';
}
$_SESSION['login_tries'] = $tries;
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Login · KartEasy Admin</title>
<link rel="stylesheet" href="assets/admin.css">
</head>
<body>
<form class="login" method="post">
<h1>🛒 KartEasy Admin</h1>
<?php if ($error): ?><div class="flash err"><?= h($error) ?></div><?php endif; ?>
<?= csrf_field() ?>
<label>Email</label>
<input type="email" name="email" required value="<?= h(post('email')) ?>" autocomplete="username">
<label>Password</label>
<input type="password" name="password" required autocomplete="current-password">
<button class="btn">Login</button>
</form>
</body>
</html><?php
require_once __DIR__ . '/inc/bootstrap.php';
$_SESSION = array();
session_destroy();
redirect('login.php');Step 4: dashboard
One query with six sub-selects fills all the number cards. Revenue excludes cancelled orders. Products with stock ≤ 5 are listed so the owner can restock.
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
$stats = $pdo->query(
"SELECT
(SELECT COUNT(*) FROM orders) AS orders,
(SELECT COUNT(*) FROM orders WHERE status = 'placed') AS pending,
(SELECT COALESCE(SUM(total), 0) FROM orders WHERE status <> 'cancelled') AS revenue,
(SELECT COUNT(*) FROM users) AS users,
(SELECT COUNT(*) FROM products WHERE is_active = 1) AS products,
(SELECT COUNT(*) FROM products WHERE is_active = 1 AND stock <= 5) AS low_stock"
)->fetch();
$recent = $pdo->query(
'SELECT o.id, o.order_no, o.total, o.status, o.created_at, u.name
FROM orders o JOIN users u ON u.id = o.user_id ORDER BY o.id DESC LIMIT 8'
)->fetchAll();
$low = $pdo->query('SELECT id, name, stock, image FROM products WHERE is_active = 1 AND stock <= 5 ORDER BY stock LIMIT 8')->fetchAll();
$pageTitle = 'Dashboard';
$active = 'index.php';
require __DIR__ . '/inc/header.php';
?>
<div class="cards">
<div class="card"><div class="n"><?= rupees($stats['revenue']) ?></div><div class="l">Revenue (excluding cancelled)</div></div>
<div class="card"><div class="n"><?= (int) $stats['orders'] ?></div><div class="l">Total orders</div></div>
<div class="card"><div class="n"><?= (int) $stats['pending'] ?></div><div class="l">New orders to confirm</div></div>
<div class="card"><div class="n"><?= (int) $stats['users'] ?></div><div class="l">Customers</div></div>
<div class="card"><div class="n"><?= (int) $stats['products'] ?></div><div class="l">Active products</div></div>
<div class="card"><div class="n"><?= (int) $stats['low_stock'] ?></div><div class="l">Low stock (≤ 5)</div></div>
</div>
<div class="box">
<h3>Recent orders</h3>
<table>
<tr><th>Order</th><th>Customer</th><th>Total</th><th>Status</th><th>Date</th></tr>
<?php foreach ($recent as $o): ?>
<tr>
<td><a href="order.php?id=<?= (int) $o['id'] ?>"><?= h($o['order_no']) ?></a></td>
<td><?= h($o['name']) ?></td>
<td><?= rupees($o['total']) ?></td>
<td><span class="badge <?= h($o['status']) ?>"><?= h($o['status']) ?></span></td>
<td class="muted"><?= h(date('d M Y, h:i A', strtotime($o['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$recent): ?><tr><td colspan="5" class="muted">No orders yet.</td></tr><?php endif; ?>
</table>
</div>
<?php if ($low): ?>
<div class="box">
<h3>Low stock</h3>
<table>
<tr><th></th><th>Product</th><th>Stock</th><th></th></tr>
<?php foreach ($low as $p): ?>
<tr>
<td><img src="<?= h(img_src($p['image'])) ?>" alt=""></td>
<td><?= h($p['name']) ?></td>
<td><strong><?= (int) $p['stock'] ?></strong></td>
<td><a class="btn sm" href="product-edit.php?id=<?= (int) $p['id'] ?>">Update</a></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
<?php require __DIR__ . '/inc/footer.php';Step 5: orders and status

<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$status = isset($_GET['status']) ? $_GET['status'] : '';
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
$where = array('1 = 1');
$params = array();
if (in_array($status, $ORDER_STATUSES, true)) {
$where[] = 'o.status = ?';
$params[] = $status;
}
if ($q !== '') {
$where[] = '(o.order_no LIKE ? OR u.name LIKE ? OR o.ship_phone LIKE ?)';
$params[] = '%' . $q . '%';
$params[] = '%' . $q . '%';
$params[] = '%' . $q . '%';
}
$stmt = db()->prepare(
'SELECT o.*, u.name AS customer,
(SELECT SUM(qty) FROM order_items i WHERE i.order_id = o.id) AS items
FROM orders o JOIN users u ON u.id = o.user_id
WHERE ' . implode(' AND ', $where) . ' ORDER BY o.id DESC LIMIT 200'
);
$stmt->execute($params);
$orders = $stmt->fetchAll();
$pageTitle = 'Orders';
$active = 'orders.php';
require __DIR__ . '/inc/header.php';
?>
<form class="toolbar" method="get">
<input type="search" name="q" value="<?= h($q) ?>" placeholder="Order no, name or phone">
<select name="status">
<option value="">All statuses</option>
<?php foreach ($ORDER_STATUSES as $s): ?>
<option value="<?= h($s) ?>" <?= $status === $s ? 'selected' : '' ?>><?= h(ucfirst($s)) ?></option>
<?php endforeach; ?>
</select>
<button class="btn gray">Filter</button>
</form>
<div class="box">
<table>
<tr><th>Order</th><th>Customer</th><th>Items</th><th>Total</th><th>Payment</th><th>Status</th><th>Date</th></tr>
<?php foreach ($orders as $o): ?>
<tr>
<td><a href="order.php?id=<?= (int) $o['id'] ?>"><?= h($o['order_no']) ?></a></td>
<td><?= h($o['customer']) ?><br><span class="muted"><?= h($o['ship_phone']) ?></span></td>
<td><?= (int) $o['items'] ?></td>
<td><?= rupees($o['total']) ?></td>
<td><?= h($o['payment_method']) ?></td>
<td><span class="badge <?= h($o['status']) ?>"><?= h($o['status']) ?></span></td>
<td class="muted"><?= h(date('d M, h:i A', strtotime($o['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$orders): ?><tr><td colspan="7" class="muted">No orders found.</td></tr><?php endif; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';
Changing the status runs in a transaction. Moving to cancelled returns the stock. A delivered or cancelled order is locked. The customer sees the new status the next time the app opens the order.
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
$new = post('status');
$pdo->beginTransaction();
$cur = $pdo->prepare('SELECT status FROM orders WHERE id = ? FOR UPDATE');
$cur->execute(array($id));
$old = $cur->fetchColumn();
if ($old === false || !in_array($new, $ORDER_STATUSES, true)) {
$pdo->rollBack();
flash('Invalid order or status.', 'err');
} elseif ($old === 'cancelled' || $old === 'delivered') {
$pdo->rollBack();
flash('A ' . $old . ' order cannot be changed.', 'err');
} else {
$pdo->prepare('UPDATE orders SET status = ? WHERE id = ?')->execute(array($new, $id));
if ($new === 'cancelled') {
// Put the stock back
$pdo->prepare(
'UPDATE products p JOIN order_items i ON i.product_id = p.id
SET p.stock = p.stock + i.qty WHERE i.order_id = ?'
)->execute(array($id));
}
$pdo->commit();
flash('Order status changed to ' . $new . '.');
}
redirect('order.php?id=' . $id);
}
$stmt = $pdo->prepare('SELECT o.*, u.name AS customer, u.email FROM orders o JOIN users u ON u.id = o.user_id WHERE o.id = ?');
$stmt->execute(array($id));
$o = $stmt->fetch();
if (!$o) {
flash('Order not found.', 'err');
redirect('orders.php');
}
$it = $pdo->prepare('SELECT * FROM order_items WHERE order_id = ?');
$it->execute(array($id));
$items = $it->fetchAll();
$pageTitle = 'Order ' . $o['order_no'];
$active = 'orders.php';
require __DIR__ . '/inc/header.php';
?>
<div class="cards">
<div class="card"><div class="l">Status</div><div class="n" style="font-size:20px"><span class="badge <?= h($o['status']) ?>"><?= h($o['status']) ?></span></div></div>
<div class="card"><div class="l">Total</div><div class="n" style="font-size:20px"><?= rupees($o['total']) ?></div></div>
<div class="card"><div class="l">Payment</div><div class="n" style="font-size:20px"><?= h($o['payment_method']) ?></div></div>
<div class="card"><div class="l">Placed on</div><div class="n" style="font-size:16px"><?= h(date('d M Y, h:i A', strtotime($o['created_at']))) ?></div></div>
</div>
<?php if (!in_array($o['status'], array('cancelled', 'delivered'), true)): ?>
<form class="box toolbar" method="post">
<?= csrf_field() ?>
<strong>Change status:</strong>
<select name="status">
<?php foreach ($ORDER_STATUSES as $s): ?>
<option value="<?= h($s) ?>" <?= $o['status'] === $s ? 'selected' : '' ?>><?= h(ucfirst($s)) ?></option>
<?php endforeach; ?>
</select>
<button class="btn">Update</button>
</form>
<?php endif; ?>
<div class="box">
<h3>Ship to</h3>
<p><strong><?= h($o['ship_name']) ?></strong> · <?= h($o['ship_phone']) ?><br><?= h($o['ship_address']) ?></p>
<p class="muted">Customer account: <?= h($o['customer']) ?> (<?= h($o['email']) ?>)</p>
</div>
<div class="box">
<table>
<tr><th></th><th>Product</th><th>Price</th><th>Qty</th><th>Amount</th></tr>
<?php foreach ($items as $i): ?>
<tr>
<td><img src="<?= h(img_src($i['image'])) ?>" alt=""></td>
<td><?= h($i['name']) ?></td>
<td><?= rupees($i['price']) ?></td>
<td><?= (int) $i['qty'] ?></td>
<td><?= rupees($i['price'] * $i['qty']) ?></td>
</tr>
<?php endforeach; ?>
<tr><td colspan="4" style="text-align:right">Subtotal</td><td><?= rupees($o['subtotal']) ?></td></tr>
<?php if ((float) $o['discount'] > 0): ?>
<tr><td colspan="4" style="text-align:right">Discount (<?= h($o['coupon_code']) ?>)</td><td>− <?= rupees($o['discount']) ?></td></tr>
<?php endif; ?>
<tr><td colspan="4" style="text-align:right">Delivery</td><td><?= (float) $o['delivery_fee'] > 0 ? rupees($o['delivery_fee']) : 'Free' ?></td></tr>
<tr><td colspan="4" style="text-align:right"><strong>Total</strong></td><td><strong><?= rupees($o['total']) ?></strong></td></tr>
</table>
</div>
<a class="btn gray" href="orders.php">← All orders</a>
<?php require __DIR__ . '/inc/footer.php';Step 6: products with image upload
Products are never deleted — the Hide button flips is_active, so old orders stay correct. The edit form validates price ≤ MRP and stock ≥ 0, uploads a main image and optional gallery images.
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
// Show / hide a product. We never DELETE products because old orders point to them.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
$pdo->prepare('UPDATE products SET is_active = 1 - is_active WHERE id = ?')->execute(array((int) post('id')));
flash('Product visibility changed.');
redirect('products.php?' . http_build_query($_GET));
}
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
$cat = isset($_GET['category']) ? (int) $_GET['category'] : 0;
$where = array('1 = 1');
$params = array();
if ($q !== '') {
$where[] = 'p.name LIKE ?';
$params[] = '%' . $q . '%';
}
if ($cat > 0) {
$where[] = 'p.category_id = ?';
$params[] = $cat;
}
$stmt = $pdo->prepare(
'SELECT p.*, c.name AS category FROM products p JOIN categories c ON c.id = p.category_id
WHERE ' . implode(' AND ', $where) . ' ORDER BY p.id DESC'
);
$stmt->execute($params);
$products = $stmt->fetchAll();
$categories = $pdo->query('SELECT id, name FROM categories ORDER BY sort_order, name')->fetchAll();
$pageTitle = 'Products';
$active = 'products.php';
require __DIR__ . '/inc/header.php';
?>
<form class="toolbar" method="get">
<input type="search" name="q" value="<?= h($q) ?>" placeholder="Search products">
<select name="category">
<option value="0">All categories</option>
<?php foreach ($categories as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= $cat === (int) $c['id'] ? 'selected' : '' ?>><?= h($c['name']) ?></option>
<?php endforeach; ?>
</select>
<button class="btn gray">Filter</button>
<a class="btn" href="product-edit.php" style="margin-left:auto">+ Add product</a>
</form>
<div class="box">
<table>
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th>MRP</th><th>Stock</th><th>Status</th><th></th></tr>
<?php foreach ($products as $p): ?>
<tr>
<td><img src="<?= h(img_src($p['image'])) ?>" alt=""></td>
<td><?= h($p['name']) ?><?= $p['is_featured'] ? ' <span class="badge">Featured</span>' : '' ?></td>
<td><?= h($p['category']) ?></td>
<td><?= rupees($p['price']) ?></td>
<td class="muted"><?= rupees($p['mrp']) ?></td>
<td><?= (int) $p['stock'] ?></td>
<td><?= $p['is_active'] ? '<span class="badge delivered">Live</span>' : '<span class="badge off">Hidden</span>' ?></td>
<td style="white-space:nowrap">
<a class="btn sm" href="product-edit.php?id=<?= (int) $p['id'] ?>">Edit</a>
<form class="inline" method="post">
<?= csrf_field() ?><input type="hidden" name="id" value="<?= (int) $p['id'] ?>">
<button class="btn sm gray"><?= $p['is_active'] ? 'Hide' : 'Show' ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$products): ?><tr><td colspan="8" class="muted">No products found.</td></tr><?php endif; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$p = array('category_id' => 0, 'name' => '', 'short_desc' => '', 'description' => '', 'price' => '', 'mrp' => '',
'stock' => 0, 'image' => null, 'is_featured' => 0, 'is_active' => 1);
if ($id > 0) {
$stmt = $pdo->prepare('SELECT * FROM products WHERE id = ?');
$stmt->execute(array($id));
$p = $stmt->fetch();
if (!$p) {
flash('Product not found.', 'err');
redirect('products.php');
}
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
// Delete one gallery image
if (post('delete_image') !== '') {
$pdo->prepare('DELETE FROM product_images WHERE id = ? AND product_id = ?')->execute(array((int) post('delete_image'), $id));
flash('Image removed.');
redirect('product-edit.php?id=' . $id);
}
$data = array(
'category_id' => (int) post('category_id'),
'name' => post('name'),
'short_desc' => post('short_desc'),
'description' => post('description'),
'price' => (float) post('price'),
'mrp' => (float) post('mrp'),
'stock' => (int) post('stock'),
'is_featured' => isset($_POST['is_featured']) ? 1 : 0,
'is_active' => isset($_POST['is_active']) ? 1 : 0,
);
if ($data['name'] === '' || $data['category_id'] <= 0) {
$error = 'Name and category are required.';
} elseif ($data['price'] <= 0 || $data['mrp'] < $data['price']) {
$error = 'Price must be more than 0 and MRP must be at least the price.';
} elseif ($data['stock'] < 0) {
$error = 'Stock cannot be negative.';
}
if ($error === '') {
try {
$image = save_upload('image', 'products');
if ($image !== null) {
$data['image'] = $image;
} elseif ($id === 0) {
throw new RuntimeException('Please choose a product image.');
}
$cols = array_keys($data);
if ($id > 0) {
$set = implode(' = ?, ', $cols) . ' = ?';
$vals = array_values($data);
$vals[] = $id;
$pdo->prepare("UPDATE products SET $set WHERE id = ?")->execute($vals);
} else {
$cols[] = 'slug';
$vals = array_values($data);
$vals[] = slugify($data['name']) . '-' . substr(bin2hex(random_bytes(3)), 0, 4);
$marks = implode(', ', array_fill(0, count($cols), '?'));
$pdo->prepare('INSERT INTO products (' . implode(', ', $cols) . ") VALUES ($marks)")->execute($vals);
$id = (int) $pdo->lastInsertId();
}
// Extra gallery image (optional)
$extra = save_upload('gallery', 'products');
if ($extra !== null) {
$pdo->prepare('INSERT INTO product_images (product_id, image, sort_order) VALUES (?, ?, 99)')->execute(array($id, $extra));
}
flash('Product saved.');
redirect('product-edit.php?id=' . $id);
} catch (RuntimeException $e) {
$error = $e->getMessage();
}
}
$p = array_merge($p, $data);
}
$categories = $pdo->query('SELECT id, name FROM categories ORDER BY sort_order, name')->fetchAll();
$gallery = array();
if ($id > 0) {
$g = $pdo->prepare('SELECT id, image FROM product_images WHERE product_id = ? ORDER BY sort_order, id');
$g->execute(array($id));
$gallery = $g->fetchAll();
}
$pageTitle = $id > 0 ? 'Edit product' : 'Add product';
$active = 'products.php';
require __DIR__ . '/inc/header.php';
?>
<?php if ($error): ?><div class="flash err"><?= h($error) ?></div><?php endif; ?>
<form class="box form" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="full"><label>Product name</label><input name="name" required maxlength="150" value="<?= h($p['name']) ?>"></div>
<div>
<label>Category</label>
<select name="category_id" required>
<option value="">Choose…</option>
<?php foreach ($categories as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= (int) $p['category_id'] === (int) $c['id'] ? 'selected' : '' ?>><?= h($c['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div><label>Stock</label><input type="number" name="stock" min="0" value="<?= (int) $p['stock'] ?>"></div>
<div><label>Selling price (₹)</label><input type="number" step="0.01" name="price" required value="<?= h($p['price']) ?>"></div>
<div><label>MRP (₹)</label><input type="number" step="0.01" name="mrp" required value="<?= h($p['mrp']) ?>"></div>
<div class="full"><label>Short description (shown on cards)</label><input name="short_desc" maxlength="255" value="<?= h($p['short_desc']) ?>"></div>
<div class="full"><label>Full description</label><textarea name="description"><?= h($p['description']) ?></textarea></div>
<div>
<label>Main image <?= $id ? '(leave empty to keep)' : '' ?></label>
<?php if ($p['image']): ?><img src="<?= h(img_src($p['image'])) ?>" alt="" style="width:90px;border-radius:8px;display:block;margin-bottom:6px"><?php endif; ?>
<input type="file" name="image" accept="image/png,image/jpeg,image/webp">
</div>
<div><label>Add a gallery image (optional)</label><input type="file" name="gallery" accept="image/png,image/jpeg,image/webp"></div>
<div class="check"><input type="checkbox" id="f" name="is_featured" <?= $p['is_featured'] ? 'checked' : '' ?>><label for="f">Featured on home</label></div>
<div class="check"><input type="checkbox" id="a" name="is_active" <?= $p['is_active'] ? 'checked' : '' ?>><label for="a">Visible in app</label></div>
<div class="full"><button class="btn">Save product</button> <a class="btn gray" href="products.php">Back</a></div>
</form>
<?php if ($gallery): ?>
<div class="box">
<h3>Gallery</h3>
<?php foreach ($gallery as $g): ?>
<form class="inline" method="post" style="display:inline-block;margin:0 10px 10px 0;text-align:center">
<?= csrf_field() ?><input type="hidden" name="delete_image" value="<?= (int) $g['id'] ?>">
<img src="<?= h(img_src($g['image'])) ?>" alt="" style="width:90px;height:90px;object-fit:cover;border-radius:8px;display:block">
<button class="btn sm red" onclick="return confirm('Remove this image?')">Remove</button>
</form>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php require __DIR__ . '/inc/footer.php';Step 7: categories, banners, coupons, customers

<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
if (post('toggle') !== '') {
$pdo->prepare('UPDATE banners SET is_active = 1 - is_active WHERE id = ?')->execute(array((int) post('toggle')));
flash('Banner visibility changed.');
redirect('banners.php');
}
if (post('delete') !== '') {
$pdo->prepare('DELETE FROM banners WHERE id = ?')->execute(array((int) post('delete')));
flash('Banner deleted.');
redirect('banners.php');
}
try {
$title = post('title');
$type = post('link_type', 'none');
if (!in_array($type, array('none', 'category', 'product'), true)) {
$type = 'none';
}
$linkId = $type === 'none' ? null : (int) post('link_id');
if ($title === '') {
throw new RuntimeException('Title is required.');
}
$image = save_upload('image', 'banners');
if ($image === null) {
throw new RuntimeException('Please choose a banner image (recommended size 1200 × 500).');
}
$pdo->prepare('INSERT INTO banners (title, subtitle, image, link_type, link_id, sort_order) VALUES (?, ?, ?, ?, ?, ?)')
->execute(array($title, post('subtitle'), $image, $type, $linkId, (int) post('sort_order')));
flash('Banner added.');
} catch (RuntimeException $e) {
flash($e->getMessage(), 'err');
}
redirect('banners.php');
}
$rows = $pdo->query('SELECT * FROM banners ORDER BY sort_order, id')->fetchAll();
$pageTitle = 'Home banners';
$active = 'banners.php';
require __DIR__ . '/inc/header.php';
?>
<form class="box form" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<h3 class="full" style="margin:0">Add banner</h3>
<div><label>Title</label><input name="title" required maxlength="100"></div>
<div><label>Subtitle</label><input name="subtitle" maxlength="150"></div>
<div>
<label>On tap open</label>
<select name="link_type"><option value="none">Nothing</option><option value="category">Category</option><option value="product">Product</option></select>
</div>
<div><label>Category / product ID</label><input type="number" name="link_id" min="0"></div>
<div><label>Sort order</label><input type="number" name="sort_order" value="<?= count($rows) + 1 ?>"></div>
<div><label>Image (1200 × 500)</label><input type="file" name="image" required accept="image/png,image/jpeg,image/webp"></div>
<div class="full"><button class="btn">Add banner</button></div>
</form>
<div class="box">
<table>
<tr><th></th><th>Title</th><th>Opens</th><th>Order</th><th>Status</th><th></th></tr>
<?php foreach ($rows as $b): ?>
<tr>
<td><img src="<?= h(img_src($b['image'])) ?>" alt="" style="width:110px"></td>
<td><?= h($b['title']) ?><br><span class="muted"><?= h($b['subtitle']) ?></span></td>
<td><?= h($b['link_type']) ?><?= $b['link_id'] ? ' #' . (int) $b['link_id'] : '' ?></td>
<td><?= (int) $b['sort_order'] ?></td>
<td><?= $b['is_active'] ? '<span class="badge delivered">Live</span>' : '<span class="badge off">Hidden</span>' ?></td>
<td style="white-space:nowrap">
<form class="inline" method="post"><?= csrf_field() ?><input type="hidden" name="toggle" value="<?= (int) $b['id'] ?>"><button class="btn sm gray"><?= $b['is_active'] ? 'Hide' : 'Show' ?></button></form>
<form class="inline" method="post"><?= csrf_field() ?><input type="hidden" name="delete" value="<?= (int) $b['id'] ?>"><button class="btn sm red" onclick="return confirm('Delete this banner?')">Delete</button></form>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
if (post('toggle') !== '') {
$pdo->prepare('UPDATE coupons SET is_active = 1 - is_active WHERE id = ?')->execute(array((int) post('toggle')));
flash('Coupon status changed.');
redirect('coupons.php');
}
$code = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', post('code')));
$type = post('type') === 'percent' ? 'percent' : 'flat';
$value = (float) post('value');
$max = post('max_discount') === '' ? null : (float) post('max_discount');
$expires = post('expires_at') === '' ? null : date('Y-m-d 23:59:59', strtotime(post('expires_at')));
if ($code === '' || $value <= 0 || ($type === 'percent' && $value > 90)) {
flash('Enter a code and a value (percent coupons: 1–90).', 'err');
redirect('coupons.php');
}
$exists = $pdo->prepare('SELECT id FROM coupons WHERE code = ?');
$exists->execute(array($code));
if ($exists->fetch()) {
flash('Coupon ' . $code . ' already exists.', 'err');
redirect('coupons.php');
}
$pdo->prepare('INSERT INTO coupons (code, type, value, min_order, max_discount, expires_at) VALUES (?, ?, ?, ?, ?, ?)')
->execute(array($code, $type, $value, (float) post('min_order'), $max, $expires));
flash('Coupon ' . $code . ' created.');
redirect('coupons.php');
}
$rows = $pdo->query(
'SELECT c.*, (SELECT COUNT(*) FROM orders o WHERE o.coupon_code = c.code) AS used
FROM coupons c ORDER BY c.id DESC'
)->fetchAll();
$pageTitle = 'Coupons';
$active = 'coupons.php';
require __DIR__ . '/inc/header.php';
?>
<form class="box form" method="post">
<?= csrf_field() ?>
<h3 class="full" style="margin:0">New coupon</h3>
<div><label>Code</label><input name="code" required maxlength="30" placeholder="DIWALI20" style="text-transform:uppercase"></div>
<div><label>Type</label><select name="type"><option value="flat">Flat ₹ off</option><option value="percent">Percent % off</option></select></div>
<div><label>Value</label><input type="number" step="0.01" name="value" required></div>
<div><label>Minimum order (₹)</label><input type="number" step="0.01" name="min_order" value="0"></div>
<div><label>Max discount (₹, for percent)</label><input type="number" step="0.01" name="max_discount"></div>
<div><label>Expires on</label><input type="date" name="expires_at"></div>
<div class="full"><button class="btn">Create coupon</button></div>
</form>
<div class="box">
<table>
<tr><th>Code</th><th>Offer</th><th>Min order</th><th>Expires</th><th>Used</th><th>Status</th><th></th></tr>
<?php foreach ($rows as $c): ?>
<tr>
<td><strong><?= h($c['code']) ?></strong></td>
<td><?= $c['type'] === 'percent' ? (float) $c['value'] . '%' . ($c['max_discount'] !== null ? ' (max ' . rupees($c['max_discount']) . ')' : '') : rupees($c['value']) ?></td>
<td><?= rupees($c['min_order']) ?></td>
<td class="muted"><?= $c['expires_at'] ? h(date('d M Y', strtotime($c['expires_at']))) : 'Never' ?></td>
<td><?= (int) $c['used'] ?></td>
<td><?= $c['is_active'] ? '<span class="badge delivered">Active</span>' : '<span class="badge off">Off</span>' ?></td>
<td><form class="inline" method="post"><?= csrf_field() ?><input type="hidden" name="toggle" value="<?= (int) $c['id'] ?>"><button class="btn sm gray"><?= $c['is_active'] ? 'Disable' : 'Enable' ?></button></form></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';
<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$pdo = db();
$editId = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_csrf();
if (post('toggle') !== '') {
$pdo->prepare('UPDATE categories SET is_active = 1 - is_active WHERE id = ?')->execute(array((int) post('toggle')));
flash('Category visibility changed.');
redirect('categories.php');
}
$id = (int) post('id');
$name = post('name');
$sort = (int) post('sort_order');
try {
if ($name === '') {
throw new RuntimeException('Category name is required.');
}
$image = save_upload('image', 'categories');
if ($id > 0) {
$pdo->prepare('UPDATE categories SET name = ?, sort_order = ? WHERE id = ?')->execute(array($name, $sort, $id));
if ($image !== null) {
$pdo->prepare('UPDATE categories SET image = ? WHERE id = ?')->execute(array($image, $id));
}
} else {
$pdo->prepare('INSERT INTO categories (name, slug, image, sort_order) VALUES (?, ?, ?, ?)')
->execute(array($name, slugify($name) . '-' . substr(bin2hex(random_bytes(2)), 0, 3), $image, $sort));
}
flash('Category saved.');
redirect('categories.php');
} catch (RuntimeException $e) {
flash($e->getMessage(), 'err');
redirect('categories.php' . ($id ? '?edit=' . $id : ''));
}
}
$rows = $pdo->query(
'SELECT c.*, (SELECT COUNT(*) FROM products p WHERE p.category_id = c.id) AS products
FROM categories c ORDER BY c.sort_order, c.name'
)->fetchAll();
$edit = array('id' => 0, 'name' => '', 'sort_order' => count($rows) + 1, 'image' => null);
foreach ($rows as $r) {
if ((int) $r['id'] === $editId) {
$edit = $r;
}
}
$pageTitle = 'Categories';
$active = 'categories.php';
require __DIR__ . '/inc/header.php';
?>
<form class="box form" method="post" enctype="multipart/form-data">
<?= csrf_field() ?><input type="hidden" name="id" value="<?= (int) $edit['id'] ?>">
<h3 class="full" style="margin:0"><?= $edit['id'] ? 'Edit category' : 'Add category' ?></h3>
<div><label>Name</label><input name="name" required maxlength="60" value="<?= h($edit['name']) ?>"></div>
<div><label>Sort order</label><input type="number" name="sort_order" value="<?= (int) $edit['sort_order'] ?>"></div>
<div class="full"><label>Icon image (square PNG works best)</label><input type="file" name="image" accept="image/png,image/jpeg,image/webp"></div>
<div class="full"><button class="btn">Save</button><?php if ($edit['id']): ?> <a class="btn gray" href="categories.php">Cancel</a><?php endif; ?></div>
</form>
<div class="box">
<table>
<tr><th></th><th>Name</th><th>Products</th><th>Order</th><th>Status</th><th></th></tr>
<?php foreach ($rows as $c): ?>
<tr>
<td><img src="<?= h(img_src($c['image'])) ?>" alt=""></td>
<td><?= h($c['name']) ?></td>
<td><?= (int) $c['products'] ?></td>
<td><?= (int) $c['sort_order'] ?></td>
<td><?= $c['is_active'] ? '<span class="badge delivered">Live</span>' : '<span class="badge off">Hidden</span>' ?></td>
<td style="white-space:nowrap">
<a class="btn sm" href="categories.php?edit=<?= (int) $c['id'] ?>">Edit</a>
<form class="inline" method="post"><?= csrf_field() ?><input type="hidden" name="toggle" value="<?= (int) $c['id'] ?>"><button class="btn sm gray"><?= $c['is_active'] ? 'Hide' : 'Show' ?></button></form>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';<?php
require_once __DIR__ . '/inc/bootstrap.php';
require_admin();
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
$sql = 'SELECT u.id, u.name, u.email, u.phone, u.created_at,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS orders,
(SELECT COALESCE(SUM(total), 0) FROM orders o WHERE o.user_id = u.id AND o.status <> \'cancelled\') AS spent
FROM users u';
$params = array();
if ($q !== '') {
$sql .= ' WHERE u.name LIKE ? OR u.email LIKE ? OR u.phone LIKE ?';
$params = array('%' . $q . '%', '%' . $q . '%', '%' . $q . '%');
}
$stmt = db()->prepare($sql . ' ORDER BY u.id DESC LIMIT 200');
$stmt->execute($params);
$users = $stmt->fetchAll();
$pageTitle = 'Customers';
$active = 'users.php';
require __DIR__ . '/inc/header.php';
?>
<form class="toolbar" method="get">
<input type="search" name="q" value="<?= h($q) ?>" placeholder="Name, email or phone">
<button class="btn gray">Search</button>
</form>
<div class="box">
<table>
<tr><th>Name</th><th>Email</th><th>Phone</th><th>Orders</th><th>Spent</th><th>Joined</th></tr>
<?php foreach ($users as $u): ?>
<tr>
<td><?= h($u['name']) ?></td>
<td><?= h($u['email']) ?></td>
<td><?= h($u['phone']) ?></td>
<td><?= (int) $u['orders'] ?></td>
<td><?= rupees($u['spent']) ?></td>
<td class="muted"><?= h(date('d M Y', strtotime($u['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php require __DIR__ . '/inc/footer.php';Change the admin password
php -r "echo password_hash('MyNew#Strong2026', PASSWORD_DEFAULT), PHP_EOL;"
-- then in phpMyAdmin → SQL:
UPDATE admins SET password_hash = '$2y$10$...paste-the-hash...' WHERE email = 'admin@karteasy.in';Change the password, use HTTPS (free SSL on Hostinger), keep DEBUG off, and never share the admin URL in the app.
