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' => $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' => $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' => $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' => $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'); ?>