fetchAll( "SELECT p.*, c.name as category_name FROM products p LEFT JOIN categories c ON p.category = c.slug ORDER BY p.name ASC" ); $cols = ['product_id','name','description','price','sale_price','cost_price', 'sku','barcode','category','tags','stock','low_stock_threshold', 'weight','is_active','is_featured','images']; ob_end_clean(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="inventory_' . date('Y-m-d') . '.csv"'); header('Cache-Control: no-cache'); echo "\xEF\xBB\xBF"; // UTF-8 BOM — makes Excel open correctly $out = fopen('php://output', 'w'); fputcsv($out, $cols); foreach ($products as $p) { fputcsv($out, array_map(fn($c) => $p[$c] ?? '', $cols)); } fclose($out); exit; } /* ──────────────────────────────────────────────────── INLINE FIELD EDIT (AJAX) ──────────────────────────────────────────────────── */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'inline_edit') { ob_end_clean(); header('Content-Type: application/json'); $pid = $_POST['product_id'] ?? ''; $field = $_POST['field'] ?? ''; $val = $_POST['value'] ?? ''; $allowed = ['stock','price','sale_price','cost_price','sku','is_active']; if (!$pid || !in_array($field, $allowed)) { echo json_encode(['error' => 'Invalid']); exit; } if ($field === 'sale_price' && trim($val) === '') $val = null; db()->update('products', [$field => $val === '' ? null : $val], 'product_id = :id', ['id' => $pid]); echo json_encode(['ok' => true, 'val' => $val]); exit; } /* ──────────────────────────────────────────────────── IMPORT (POST with file) ──────────────────────────────────────────────────── */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'import') { $mode = $_POST['mode'] ?? 'smart'; // smart | replace if (empty($_FILES['csv_file']['tmp_name'])) { setFlash('error', 'No file uploaded'); header('Location: /admin/import-export.php'); exit; } $fh = fopen($_FILES['csv_file']['tmp_name'], 'r'); // Strip UTF-8 BOM if present $bom = fread($fh, 3); if ($bom !== "\xEF\xBB\xBF") rewind($fh); $headers = fgetcsv($fh); if (!$headers) { setFlash('error', 'Could not read CSV headers'); header('Location: /admin/import-export.php'); exit; } $headers = array_map('trim', $headers); // Required: name + price if (!in_array('name', $headers) || !in_array('price', $headers)) { setFlash('error', 'CSV must have at least "name" and "price" columns'); header('Location: /admin/import-export.php'); exit; } $rows = []; while (($row = fgetcsv($fh)) !== false) { if (count($row) < 2) continue; $rows[] = array_combine($headers, array_pad($row, count($headers), '')); } fclose($fh); if (empty($rows)) { setFlash('error', 'No data rows found in CSV'); header('Location: /admin/import-export.php'); exit; } $inserted = $updated = $deleted = 0; try { // Get current product IDs $existing = array_column(db()->fetchAll("SELECT product_id FROM products"), 'product_id'); $importedIds = array_filter(array_column($rows, 'product_id')); if ($mode === 'replace') { // Wipe and reimport — warn user before this in the UI db()->query("DELETE FROM products WHERE product_id IS NOT NULL", []); foreach ($rows as $r) { $pid = (!empty($r['product_id'])) ? $r['product_id'] : generateId('prod_'); db()->insert('products', [ 'product_id' => $pid, 'name' => $r['name'], 'description' => $r['description'] ?? null, 'price' => floatval($r['price'] ?? 0), 'sale_price' => ($r['sale_price'] ?? '') !== '' ? floatval($r['sale_price']) : null, 'cost_price' => ($r['cost_price'] ?? '') !== '' ? floatval($r['cost_price']) : null, 'sku' => $r['sku'] ?? null, 'barcode' => $r['barcode'] ?? null, 'category' => $r['category'] ?? null, 'tags' => toJsonField($r['tags'] ?? null), 'stock' => intval($r['stock'] ?? 0), 'low_stock_threshold'=> intval($r['low_stock_threshold'] ?? 10), 'weight' => ($r['weight'] ?? '') !== '' ? floatval($r['weight']) : null, 'is_active' => intval($r['is_active'] ?? 1), 'is_featured' => intval($r['is_featured'] ?? 0), 'images' => toJsonField($r['images'] ?? null), ]); $inserted++; } } else { // Smart mode: update existing, insert new, delete removed foreach ($rows as $r) { $pid = $r['product_id'] ?? ''; $data = [ 'name' => $r['name'], 'description' => $r['description'] ?? null, 'price' => floatval($r['price'] ?? 0), 'sale_price' => ($r['sale_price'] ?? '') !== '' ? floatval($r['sale_price']) : null, 'cost_price' => ($r['cost_price'] ?? '') !== '' ? floatval($r['cost_price']) : null, 'sku' => $r['sku'] ?? null, 'barcode' => $r['barcode'] ?? null, 'category' => $r['category'] ?? null, 'tags' => toJsonField($r['tags'] ?? null), 'stock' => intval($r['stock'] ?? 0), 'low_stock_threshold'=> intval($r['low_stock_threshold'] ?? 10), 'weight' => ($r['weight'] ?? '') !== '' ? floatval($r['weight']) : null, 'is_active' => intval($r['is_active'] ?? 1), 'is_featured' => intval($r['is_featured'] ?? 0), 'images' => toJsonField($r['images'] ?? null), ]; if ($pid && in_array($pid, $existing)) { db()->update('products', $data, 'product_id = :id', ['id' => $pid]); $updated++; } else { $data['product_id'] = $pid ?: generateId('prod_'); db()->insert('products', $data); $inserted++; } } // Delete products not in import $toDelete = array_diff($existing, $importedIds); foreach ($toDelete as $pid) { db()->delete('products', 'product_id = :id', ['id' => $pid]); $deleted++; } } $msg = "Import complete — {$inserted} added, {$updated} updated"; if ($deleted) $msg .= ", {$deleted} removed"; setFlash('success', $msg); } catch (Exception $e) { setFlash('error', 'Import failed: ' . $e->getMessage()); } header('Location: /admin/import-export.php'); exit; } /* ──────────────────────────────────────────────────── DELETE single product ──────────────────────────────────────────────────── */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') { db()->delete('products', 'product_id = :id', ['id' => $_POST['product_id'] ?? '']); setFlash('success', 'Product deleted'); header('Location: /admin/import-export.php'); exit; } /* ──────────────────────────────────────────────────── LIST ──────────────────────────────────────────────────── */ $search = trim($_GET['search'] ?? ''); $catFilter = $_GET['category'] ?? ''; $where = ['1=1']; $params = []; if ($search) { $where[] = '(p.name LIKE :q OR p.sku LIKE :q2 OR p.category LIKE :q3)'; $params['q'] = $params['q2'] = $params['q3'] = "%$search%"; } if ($catFilter) { $where[] = 'p.category = :cat'; $params['cat'] = $catFilter; } $whereSQL = implode(' AND ', $where); $products = db()->fetchAll( "SELECT p.* FROM products p WHERE $whereSQL ORDER BY p.name ASC", $params ); $categories = db()->fetchAll("SELECT name, slug FROM categories WHERE is_active=1 ORDER BY name"); $totalProducts = db()->count('products'); $totalStock = db()->fetch("SELECT SUM(stock) as s FROM products")['s'] ?? 0; $lowStock = db()->count('products', 'stock <= low_stock_threshold AND is_active = 1'); ?>
Total Products
Total Units in Stock
Low Stock Alerts
Clear product
Click any price or stock cell to edit inline. Changes save instantly.
No products found.
Product SKU Category Price Sale Cost Stock Active Actions
Low Stock
$ ' ?> ' ?>