mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
Initial commit
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$pageTitle = 'Import / Export Inventory';
|
||||
$currentPage = 'import-export';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
EXPORT
|
||||
──────────────────────────────────────────────────── */
|
||||
if (isset($_GET['export'])) {
|
||||
$products = db()->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');
|
||||
?>
|
||||
|
||||
<div class="page-header" style="flex-wrap:wrap;gap:.75rem">
|
||||
<h1 class="page-title">Import / Export Inventory</h1>
|
||||
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
|
||||
<a href="/admin/import-export.php?export=1" class="btn btn-success">
|
||||
<i class="fas fa-file-download"></i> Export CSV
|
||||
</a>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('importPanel').classList.toggle('hidden')">
|
||||
<i class="fas fa-file-upload"></i> Import CSV
|
||||
</button>
|
||||
<a href="/admin/product-edit.php" class="btn btn-secondary">
|
||||
<i class="fas fa-plus"></i> Add Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid" style="margin-bottom:1.5rem">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-box"></i></div>
|
||||
<div><div class="stat-card-value"><?= $totalProducts ?></div><div class="stat-card-label">Total Products</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-cubes"></i></div>
|
||||
<div><div class="stat-card-value"><?= number_format($totalStock) ?></div><div class="stat-card-label">Total Units in Stock</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon <?= $lowStock > 0 ? 'warning' : 'success' ?>"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div><div class="stat-card-value"><?= $lowStock ?></div><div class="stat-card-label">Low Stock Alerts</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Panel (hidden by default) -->
|
||||
<div id="importPanel" class="hidden admin-card" style="margin-bottom:1.5rem;border:2px solid var(--admin-primary)">
|
||||
<div class="admin-card-header" style="background:rgba(255,94,26,.06)">
|
||||
<h3><i class="fas fa-file-upload"></i> Import Inventory from CSV</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:2rem">
|
||||
|
||||
<!-- Mode selector -->
|
||||
<div>
|
||||
<h4 style="margin:0 0 .75rem">Import Mode</h4>
|
||||
<label id="modeSmart" style="display:flex;align-items:flex-start;gap:.75rem;padding:1rem;border:2px solid var(--admin-primary);border-radius:var(--radius-md);cursor:pointer;margin-bottom:.75rem;background:rgba(255,94,26,.04)">
|
||||
<input type="radio" name="importMode" value="smart" checked onchange="setMode(this)" style="margin-top:.15rem;flex-shrink:0">
|
||||
<div>
|
||||
<strong>Smart Update</strong> <span class="badge badge-success" style="margin-left:.25rem">Recommended</span><br>
|
||||
<span class="text-muted" style="font-size:.85rem">Updates existing products, adds new ones, removes products not in the file. <strong>Preserves order history.</strong></span>
|
||||
</div>
|
||||
</label>
|
||||
<label id="modeReplace" style="display:flex;align-items:flex-start;gap:.75rem;padding:1rem;border:2px solid var(--color-border);border-radius:var(--radius-md);cursor:pointer">
|
||||
<input type="radio" name="importMode" value="replace" onchange="setMode(this)" style="margin-top:.15rem;flex-shrink:0">
|
||||
<div>
|
||||
<strong style="color:var(--color-error)"><i class="fas fa-exclamation-triangle"></i> Full Replace</strong><br>
|
||||
<span class="text-muted" style="font-size:.85rem">Deletes ALL existing products first, then imports. Use only if you want a clean slate.</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Upload form -->
|
||||
<div>
|
||||
<h4 style="margin:0 0 .75rem">Upload File</h4>
|
||||
<form method="POST" enctype="multipart/form-data" id="importForm" onsubmit="return confirmImport()">
|
||||
<input type="hidden" name="action" value="import">
|
||||
<input type="hidden" name="mode" id="importModeField" value="smart">
|
||||
|
||||
<div id="csvDrop" style="border:2px dashed var(--color-border);border-radius:var(--radius-md);padding:2rem;text-align:center;cursor:pointer;transition:all .2s;position:relative;margin-bottom:1rem">
|
||||
<i class="fas fa-file-csv" style="font-size:2.5rem;color:var(--admin-text-muted);display:block;margin-bottom:.5rem"></i>
|
||||
<div style="font-size:.9rem;color:var(--admin-text-muted)">Drop CSV here or <span style="color:var(--admin-primary);font-weight:600">browse</span></div>
|
||||
<div id="csvFileName" style="margin-top:.5rem;font-weight:600;color:var(--admin-primary)"></div>
|
||||
<input type="file" name="csv_file" id="csvFile" accept=".csv,text/csv" required
|
||||
style="position:absolute;inset:0;opacity:0;cursor:pointer" onchange="showFileName(this)">
|
||||
</div>
|
||||
|
||||
<div id="replaceWarning" style="display:none;background:#FEF2F2;border:1px solid #FCA5A5;border-radius:var(--radius-md);padding:.75rem 1rem;margin-bottom:1rem;font-size:.875rem;color:#991B1B">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Warning:</strong> Full Replace will permanently delete all existing products before importing. This cannot be undone.
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.75rem;align-items:center">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1">
|
||||
<i class="fas fa-upload"></i> Import Now
|
||||
</button>
|
||||
<a href="/admin/import-export.php?export=1" class="btn btn-secondary" title="Download a template first">
|
||||
<i class="fas fa-download"></i> Get Template
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-muted" style="font-size:.8rem;margin:.5rem 0 0">Tip: Export first to get the correct column format, edit in Excel, then import.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + Filter -->
|
||||
<div class="admin-card" style="margin-bottom:1rem">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end">
|
||||
<div class="form-group mb-0" style="flex:1;min-width:200px">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Name, SKU, category…"
|
||||
value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= htmlspecialchars($cat['slug']) ?>"
|
||||
<?= $catFilter === $cat['slug'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($cat['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($search || $catFilter): ?>
|
||||
<a href="/admin/import-export.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
<span class="text-muted" style="line-height:2.5rem;font-size:.875rem"><?= count($products) ?> product<?= count($products) !== 1 ? 's' : '' ?></span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span style="font-size:.85rem;color:var(--admin-text-muted)">
|
||||
<i class="fas fa-info-circle"></i> Click any price or stock cell to edit inline. Changes save instantly.
|
||||
</span>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding:0;overflow-x:auto">
|
||||
<?php if (empty($products)): ?>
|
||||
<div class="text-center text-muted" style="padding:3rem">No products found.</div>
|
||||
<?php else: ?>
|
||||
<table class="admin-table" style="min-width:900px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Category</th>
|
||||
<th style="text-align:right">Price</th>
|
||||
<th style="text-align:right">Sale</th>
|
||||
<th style="text-align:right">Cost</th>
|
||||
<th style="text-align:center">Stock</th>
|
||||
<th style="text-align:center">Active</th>
|
||||
<th style="text-align:center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($products as $p):
|
||||
$isLow = $p['stock'] <= $p['low_stock_threshold'] && $p['is_active'];
|
||||
?>
|
||||
<tr class="<?= !$p['is_active'] ? 'row-inactive' : '' ?>">
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($p['name']) ?></strong>
|
||||
<?php if ($isLow): ?>
|
||||
<span class="badge badge-warning" style="margin-left:.4rem;font-size:.7rem">Low Stock</span>
|
||||
<?php endif; ?>
|
||||
<div style="font-size:.75rem;color:var(--admin-text-muted)"><?= htmlspecialchars($p['product_id']) ?></div>
|
||||
</td>
|
||||
<td class="editable-cell" data-field="sku" data-id="<?= $p['product_id'] ?>">
|
||||
<span class="cell-display"><?= htmlspecialchars($p['sku'] ?? '—') ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:90px;padding:.2rem .4rem;font-size:.85rem" value="<?= htmlspecialchars($p['sku'] ?? '') ?>">
|
||||
</td>
|
||||
<td><?= htmlspecialchars($p['category'] ?? '—') ?></td>
|
||||
<td class="editable-cell" data-field="price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display">$<?= number_format($p['price'], 2) ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['price'] ?>">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="sale_price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display"><?= $p['sale_price'] ? '$'.number_format($p['sale_price'],2) : '<span style="color:var(--admin-text-muted)">—</span>' ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['sale_price'] ?? '' ?>" placeholder="none">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="cost_price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display"><?= $p['cost_price'] ? '$'.number_format($p['cost_price'],2) : '<span style="color:var(--admin-text-muted)">—</span>' ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['cost_price'] ?? '' ?>" placeholder="none">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="stock" data-id="<?= $p['product_id'] ?>" style="text-align:center">
|
||||
<span class="cell-display" style="font-weight:600;color:<?= $isLow ? 'var(--color-warning)' : 'inherit' ?>"><?= $p['stock'] ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:70px;padding:.2rem .4rem;font-size:.85rem;text-align:center" value="<?= $p['stock'] ?>">
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<label class="toggle-switch" title="Toggle active">
|
||||
<input type="checkbox" class="active-toggle"
|
||||
data-id="<?= $p['product_id'] ?>"
|
||||
<?= $p['is_active'] ? 'checked' : '' ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td style="text-align:center;white-space:nowrap">
|
||||
<a href="/admin/product-edit.php?id=<?= $p['product_id'] ?>" class="btn btn-sm btn-secondary" title="Full Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="product_id" value="<?= $p['product_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete <?= htmlspecialchars(addslashes($p['name'])) ?>?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.row-inactive td { opacity: .5; }
|
||||
|
||||
.editable-cell {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.editable-cell:hover .cell-display {
|
||||
background: rgba(255,94,26,.08);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
outline: 1px dashed var(--admin-primary);
|
||||
}
|
||||
.editable-cell.editing .cell-display { display: none; }
|
||||
.editable-cell.editing .cell-input { display: inline-block !important; }
|
||||
.editable-cell .saving-dot {
|
||||
display: none;
|
||||
color: var(--admin-primary);
|
||||
font-size: .75rem;
|
||||
margin-left: .3rem;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.toggle-switch {
|
||||
position: relative; display: inline-block;
|
||||
width: 36px; height: 20px; cursor: pointer;
|
||||
}
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle-slider {
|
||||
position: absolute; inset: 0;
|
||||
background: var(--color-border); border-radius: 20px;
|
||||
transition: .2s;
|
||||
}
|
||||
.toggle-slider::before {
|
||||
content: ''; position: absolute;
|
||||
width: 14px; height: 14px; left: 3px; bottom: 3px;
|
||||
background: white; border-radius: 50%; transition: .2s;
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider { background: var(--admin-primary); }
|
||||
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
|
||||
|
||||
#csvDrop.drag-active { border-color: var(--admin-primary); background: rgba(255,94,26,.04); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ── Import panel mode ───────────────────────────── */
|
||||
function setMode(radio) {
|
||||
document.getElementById('importModeField').value = radio.value;
|
||||
document.getElementById('replaceWarning').style.display = radio.value === 'replace' ? '' : 'none';
|
||||
document.getElementById('modeSmart').style.borderColor = radio.value === 'smart' ? 'var(--admin-primary)' : 'var(--color-border)';
|
||||
document.getElementById('modeReplace').style.borderColor = radio.value === 'replace' ? 'var(--color-error)' : 'var(--color-border)';
|
||||
}
|
||||
|
||||
function showFileName(input) {
|
||||
if (input.files[0]) {
|
||||
document.getElementById('csvFileName').textContent = input.files[0].name;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmImport() {
|
||||
var mode = document.getElementById('importModeField').value;
|
||||
if (mode === 'replace') {
|
||||
return confirm('WARNING: Full Replace will DELETE all existing products and reimport from the CSV.\n\nThis cannot be undone. Are you sure?');
|
||||
}
|
||||
var file = document.getElementById('csvFile').files[0];
|
||||
if (!file) { alert('Please select a CSV file.'); return false; }
|
||||
return confirm('Import ' + file.name + '?\n\nSmart Update will update existing products and add/remove to match the file.');
|
||||
}
|
||||
|
||||
/* ── CSV drag & drop ─────────────────────────────── */
|
||||
var drop = document.getElementById('csvDrop');
|
||||
['dragenter','dragover'].forEach(function(e) {
|
||||
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.add('drag-active'); });
|
||||
});
|
||||
['dragleave','drop'].forEach(function(e) {
|
||||
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.remove('drag-active'); });
|
||||
});
|
||||
drop.addEventListener('drop', function(ev) {
|
||||
var f = ev.dataTransfer.files[0];
|
||||
if (f) {
|
||||
var dt = new DataTransfer();
|
||||
dt.items.add(f);
|
||||
document.getElementById('csvFile').files = dt.files;
|
||||
document.getElementById('csvFileName').textContent = f.name;
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Inline cell editing ─────────────────────────── */
|
||||
document.querySelectorAll('.editable-cell').forEach(function(cell) {
|
||||
var display = cell.querySelector('.cell-display');
|
||||
var input = cell.querySelector('.cell-input');
|
||||
var pid = cell.dataset.id;
|
||||
var field = cell.dataset.field;
|
||||
|
||||
cell.addEventListener('click', function(e) {
|
||||
if (cell.classList.contains('editing')) return;
|
||||
cell.classList.add('editing');
|
||||
input.style.display = 'inline-block';
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
|
||||
function save() {
|
||||
cell.classList.remove('editing');
|
||||
var val = input.value;
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'inline_edit');
|
||||
fd.append('product_id', pid);
|
||||
fd.append('field', field);
|
||||
fd.append('value', val);
|
||||
|
||||
fetch('/admin/import-export.php', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
// Refresh display text
|
||||
if (field === 'sku') {
|
||||
display.textContent = val || '—';
|
||||
} else if (field === 'stock') {
|
||||
display.textContent = val;
|
||||
} else {
|
||||
display.innerHTML = val !== '' ? '$' + parseFloat(val).toFixed(2) : '<span style="color:var(--admin-text-muted)">—</span>';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener('blur', save);
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
||||
if (e.key === 'Escape') { cell.classList.remove('editing'); input.value = input.defaultValue; }
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Active toggle ───────────────────────────────── */
|
||||
document.querySelectorAll('.active-toggle').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'inline_edit');
|
||||
fd.append('product_id', this.dataset.id);
|
||||
fd.append('field', 'is_active');
|
||||
fd.append('value', this.checked ? 1 : 0);
|
||||
fetch('/admin/import-export.php', { method: 'POST', body: fd });
|
||||
var row = this.closest('tr');
|
||||
if (row) row.classList.toggle('row-inactive', !this.checked);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
Reference in New Issue
Block a user