Files
tomsjavajive/admin/import-export.php
T
2026-05-22 12:52:44 +00:00

583 lines
29 KiB
PHP

<?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'; ?>