mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
583 lines
29 KiB
PHP
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'; ?>
|