mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
401 lines
20 KiB
PHP
401 lines
20 KiB
PHP
<?php
|
|
ob_start();
|
|
/**
|
|
* Tom's Java Jive - Admin Product Edit/Add
|
|
*/
|
|
|
|
$pageTitle = 'Product';
|
|
require_once __DIR__ . '/includes/header.php';
|
|
|
|
// Load categories and product types for dropdowns
|
|
$categoriesList = db()->fetchAll("SELECT category_id, name FROM categories WHERE is_active = 1 ORDER BY name ASC");
|
|
$productTypesList = db()->fetchAll("SELECT type_id, name FROM product_types WHERE is_active = 1 ORDER BY sort_order ASC, name ASC");
|
|
|
|
|
|
$productId = $_GET['id'] ?? '';
|
|
$product = null;
|
|
$isEdit = false;
|
|
$errors = [];
|
|
|
|
if ($productId) {
|
|
$product = db()->fetch("SELECT * FROM products WHERE product_id = :id", ['id' => $productId]);
|
|
if ($product) {
|
|
$isEdit = true;
|
|
$pageTitle = 'Edit Product';
|
|
$product['images'] = json_decode($product['images'] ?? '[]', true);
|
|
$product['tags'] = json_decode($product['tags'] ?? '[]', true);
|
|
}
|
|
}
|
|
|
|
if (!$product) {
|
|
$product = [
|
|
'product_id' => '',
|
|
'name' => '',
|
|
'description' => '',
|
|
'price' => '',
|
|
'sale_price' => '',
|
|
'cost_price' => '',
|
|
'sku' => '',
|
|
'barcode' => '',
|
|
'category' => '',
|
|
'tags' => [],
|
|
'images' => [],
|
|
'stock' => 100,
|
|
'low_stock_threshold' => 10,
|
|
'weight' => '',
|
|
'is_active' => 1,
|
|
'is_featured' => 0
|
|
];
|
|
$pageTitle = 'Add Product';
|
|
}
|
|
|
|
// Handle form submission
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$name = trim($_POST['name'] ?? '');
|
|
$description = trim($_POST['description'] ?? '');
|
|
$price = floatval($_POST['price'] ?? 0);
|
|
$salePrice = !empty($_POST['sale_price']) ? floatval($_POST['sale_price']) : null;
|
|
$costPrice = !empty($_POST['cost_price']) ? floatval($_POST['cost_price']) : null;
|
|
$sku = trim($_POST['sku'] ?? '');
|
|
$barcode = trim($_POST['barcode'] ?? '');
|
|
$category = trim($_POST['category'] ?? '');
|
|
$stock = intval($_POST['stock'] ?? 0);
|
|
$lowStockThreshold = intval($_POST['low_stock_threshold'] ?? 10);
|
|
$weight = !empty($_POST['weight']) ? floatval($_POST['weight']) : null;
|
|
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
|
$isFeatured = isset($_POST['is_featured']) ? 1 : 0;
|
|
$imageUrls = array_filter(array_map('trim', explode("\n", $_POST['image_urls'] ?? '')));
|
|
|
|
// Validate
|
|
if (empty($name)) $errors['name'] = 'Product name is required';
|
|
if ($price <= 0) $errors['price'] = 'Price must be greater than 0';
|
|
|
|
if (empty($errors)) {
|
|
$data = [
|
|
'name' => $name,
|
|
'description' => $description,
|
|
'price' => $price,
|
|
'sale_price' => $salePrice,
|
|
'cost_price' => $costPrice,
|
|
'sku' => $sku ?: null,
|
|
'barcode' => $barcode ?: null,
|
|
'category' => $category ?: null,
|
|
'product_type_id' => trim($_POST['product_type_id'] ?? '') ?: null,
|
|
'images' => json_encode($imageUrls),
|
|
'stock' => $stock,
|
|
'low_stock_threshold' => $lowStockThreshold,
|
|
'weight' => $weight,
|
|
'is_active' => $isActive,
|
|
'is_featured' => $isFeatured
|
|
];
|
|
|
|
if ($isEdit) {
|
|
db()->update('products', $data, 'product_id = :id', ['id' => $productId]);
|
|
setFlash('success', 'Product updated successfully');
|
|
} else {
|
|
$data['product_id'] = generateId('prod_');
|
|
db()->insert('products', $data);
|
|
setFlash('success', 'Product created successfully');
|
|
}
|
|
|
|
header('Location: /admin/products.php');
|
|
exit;
|
|
}
|
|
|
|
// Keep form values on error
|
|
$product = array_merge($product, $_POST);
|
|
$product['images'] = $imageUrls;
|
|
}
|
|
|
|
// Get categories for dropdown
|
|
$categories = db()->fetchAll(
|
|
"SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category"
|
|
);
|
|
?>
|
|
|
|
<div class="page-header">
|
|
<h1 class="page-title"><?= $isEdit ? 'Edit' : 'Add' ?> Product</h1>
|
|
<a href="/admin/products.php" class="btn btn-secondary">
|
|
<i class="fas fa-arrow-left"></i> Back to Products
|
|
</a>
|
|
</div>
|
|
|
|
<?php if (!empty($errors)): ?>
|
|
<div class="alert alert-error">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
Please fix the errors below
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="POST" action="">
|
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; align-items: start;">
|
|
<!-- Main Info -->
|
|
<div>
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Basic Information</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Product Name *</label>
|
|
<input type="text" name="name" class="form-input"
|
|
value="<?= htmlspecialchars($product['name']) ?>" required>
|
|
<?php if (isset($errors['name'])): ?>
|
|
<span class="form-error"><?= $errors['name'] ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<textarea name="description" class="form-textarea" rows="4"><?= htmlspecialchars($product['description']) ?></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Category</label>
|
|
<select name="category" class="form-input">
|
|
<option value="">-- Select Category --</option>
|
|
<?php foreach ($categoriesList as $cat): ?>
|
|
<option value="<?= htmlspecialchars($cat['name']) ?>"
|
|
<?= ($product['category'] ?? '') === $cat['name'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($cat['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Product Type</label>
|
|
<select name="product_type_id" class="form-input">
|
|
<option value="">-- Select Type --</option>
|
|
<?php foreach ($productTypesList as $pt): ?>
|
|
<option value="<?= htmlspecialchars($pt['type_id']) ?>"
|
|
<?= ($product['product_type_id'] ?? '') === $pt['type_id'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($pt['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Weight (oz)</label>
|
|
<input type="number" name="weight" class="form-input" step="0.01"
|
|
value="<?= htmlspecialchars($product['weight']) ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Pricing</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Price *</label>
|
|
<input type="number" name="price" class="form-input" step="0.01" min="0"
|
|
value="<?= htmlspecialchars($product['price']) ?>" required>
|
|
<?php if (isset($errors['price'])): ?>
|
|
<span class="form-error"><?= $errors['price'] ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Sale Price</label>
|
|
<input type="number" name="sale_price" class="form-input" step="0.01" min="0"
|
|
value="<?= htmlspecialchars($product['sale_price'] ?? '') ?>">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Cost Price (for profit tracking)</label>
|
|
<input type="number" name="cost_price" class="form-input" step="0.01" min="0"
|
|
value="<?= htmlspecialchars($product['cost_price'] ?? '') ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Images</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-group">
|
|
<label class="form-label"><div class="form-group">
|
|
<label class="form-label">Product Images</label>
|
|
|
|
<!-- Drag & Drop Upload Zone -->
|
|
<div id="dropZone" style="border:2px dashed var(--color-border);border-radius:var(--radius-md);padding:2rem;text-align:center;cursor:pointer;transition:all .2s;margin-bottom:1rem;background:var(--color-background)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)" onclick="document.getElementById('imageFileInput').click()">
|
|
<i class="fas fa-cloud-upload-alt" style="font-size:2rem;color:var(--color-text-muted);margin-bottom:.5rem;display:block"></i>
|
|
<div style="font-weight:600;margin-bottom:.25rem">Drag & drop images here</div>
|
|
<div style="font-size:.875rem;color:var(--color-text-muted)">or click to browse — JPG, PNG, WebP, GIF up to 5MB each</div>
|
|
<input type="file" id="imageFileInput" multiple accept="image/*" style="display:none" onchange="handleFileSelect(this.files)">
|
|
</div>
|
|
|
|
<!-- Upload Preview -->
|
|
<div id="uploadPreviews" style="display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem"></div>
|
|
|
|
<!-- URL Input -->
|
|
<div style="position:relative;margin-bottom:.5rem">
|
|
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;color:var(--color-text-muted);font-size:.875rem">
|
|
<hr style="flex:1;border:none;border-top:1px solid var(--color-border)">
|
|
<span>or add image URLs</span>
|
|
<hr style="flex:1;border:none;border-top:1px solid var(--color-border)">
|
|
</div>
|
|
<textarea name="images" id="imageUrls" class="form-textarea" rows="3" placeholder="https://example.com/image1.jpg https://example.com/image2.jpg"><?= htmlspecialchars(is_array($product['images']) ? implode("
|
|
", $product['images']) : ($product['images'] ?? '')) ?></textarea>
|
|
<small class="text-muted">Enter one URL per line</small>
|
|
</div>
|
|
|
|
<script>
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
document.getElementById('dropZone').style.borderColor = 'var(--color-primary)';
|
|
document.getElementById('dropZone').style.background = 'rgba(255,94,26,.05)';
|
|
}
|
|
function handleDragLeave(e) {
|
|
document.getElementById('dropZone').style.borderColor = 'var(--color-border)';
|
|
document.getElementById('dropZone').style.background = 'var(--color-background)';
|
|
}
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
handleDragLeave(e);
|
|
handleFileSelect(e.dataTransfer.files);
|
|
}
|
|
function handleFileSelect(files) {
|
|
Array.from(files).forEach(file => {
|
|
if (!file.type.startsWith('image/')) return;
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
alert(file.name + ' is too large (max 5MB)');
|
|
return;
|
|
}
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
uploadImage(file, e.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
function uploadImage(file, dataUrl) {
|
|
var preview = document.createElement('div');
|
|
preview.style.cssText = 'position:relative;width:100px;height:100px;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--color-border)';
|
|
preview.innerHTML = '<img src="' + dataUrl + '" style="width:100%;height:100%;object-fit:cover">' +
|
|
'<div style="position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center">' +
|
|
'<i class="fas fa-spinner fa-spin" style="color:#fff;font-size:1.25rem"></i></div>';
|
|
document.getElementById('uploadPreviews').appendChild(preview);
|
|
|
|
var formData = new FormData();
|
|
formData.append('image', file);
|
|
formData.append('action', 'upload_image');
|
|
|
|
fetch('/admin/upload-image.php', {method:'POST', body:formData})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.url) {
|
|
preview.innerHTML = '<img src="' + data.url + '" style="width:100%;height:100%;object-fit:cover">' +
|
|
'<button type="button" onclick="removePreview(this, '' + data.url + '')" style="position:absolute;top:2px;right:2px;background:rgba(0,0,0,.6);border:none;color:#fff;border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:.7rem;display:flex;align-items:center;justify-content:center">×</button>';
|
|
var urls = document.getElementById('imageUrls');
|
|
urls.value = (urls.value ? urls.value + '
|
|
' : '') + data.url;
|
|
} else {
|
|
preview.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:.75rem;color:var(--color-error);padding:.5rem;text-align:center">' + (data.error || 'Upload failed') + '</div>';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
preview.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:.75rem;color:var(--color-error);padding:.5rem;text-align:center">Upload failed</div>';
|
|
});
|
|
}
|
|
function removePreview(btn, url) {
|
|
btn.closest('div').remove();
|
|
var urls = document.getElementById('imageUrls');
|
|
urls.value = urls.value.split('
|
|
').filter(u => u.trim() !== url.trim()).join('
|
|
');
|
|
}
|
|
</script>
|
|
</div>
|
|
|
|
<?php if (!empty($product['images'])): ?>
|
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
|
|
<?php foreach ($product['images'] as $img): ?>
|
|
<img src="<?= htmlspecialchars($img) ?>" alt="Product image"
|
|
style="width: 80px; height: 80px; object-fit: cover; border-radius: var(--admin-radius);">
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div>
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Status</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-group">
|
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
<input type="checkbox" name="is_active" value="1"
|
|
<?= $product['is_active'] ? 'checked' : '' ?>>
|
|
Active (visible in store)
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group mb-0">
|
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
<input type="checkbox" name="is_featured" value="1"
|
|
<?= $product['is_featured'] ? 'checked' : '' ?>>
|
|
Featured product
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Inventory</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Stock Quantity</label>
|
|
<input type="number" name="stock" class="form-input" min="0"
|
|
value="<?= htmlspecialchars($product['stock']) ?>">
|
|
</div>
|
|
|
|
<div class="form-group mb-0">
|
|
<label class="form-label">Low Stock Alert Threshold</label>
|
|
<input type="number" name="low_stock_threshold" class="form-input" min="0"
|
|
value="<?= htmlspecialchars($product['low_stock_threshold']) ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Identifiers</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<div class="form-group">
|
|
<label class="form-label">SKU</label>
|
|
<input type="text" name="sku" class="form-input"
|
|
value="<?= htmlspecialchars($product['sku'] ?? '') ?>">
|
|
</div>
|
|
|
|
<div class="form-group mb-0">
|
|
<label class="form-label">Barcode</label>
|
|
<input type="text" name="barcode" class="form-input"
|
|
value="<?= htmlspecialchars($product['barcode'] ?? '') ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
|
<i class="fas fa-save"></i> <?= $isEdit ? 'Update' : 'Create' ?> Product
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|