mirror of
https://github.com/myronblair/tomsjavajive-app
synced 2026-06-30 17:50:56 -05:00
338 lines
15 KiB
PHP
338 lines
15 KiB
PHP
<?php
|
|
ob_start();
|
|
/**
|
|
* Tom's Java Jive - Admin Inventory Management
|
|
*/
|
|
|
|
$pageTitle = 'Inventory';
|
|
require_once __DIR__ . '/includes/header.php';
|
|
|
|
// Handle actions
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
if ($action === 'adjust' && !empty($_POST['product_id'])) {
|
|
$adjustment = intval($_POST['adjustment'] ?? 0);
|
|
$reason = trim($_POST['reason'] ?? '');
|
|
|
|
if ($adjustment != 0) {
|
|
$product = db()->fetch("SELECT name, stock FROM products WHERE product_id = :id", ['id' => $_POST['product_id']]);
|
|
$newStock = max(0, ($product['stock'] ?? 0) + $adjustment);
|
|
|
|
db()->update('products', ['stock' => $newStock], 'product_id = :id', ['id' => $_POST['product_id']]);
|
|
setFlash('success', $product['name'] . ' stock adjusted by ' . ($adjustment > 0 ? '+' : '') . $adjustment . '. New stock: ' . $newStock);
|
|
}
|
|
header('Location: /admin/inventory.php');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'update_threshold' && !empty($_POST['product_id'])) {
|
|
$threshold = intval($_POST['low_stock_threshold'] ?? 10);
|
|
db()->update('products', ['low_stock_threshold' => $threshold], 'product_id = :id', ['id' => $_POST['product_id']]);
|
|
setFlash('success', 'Low stock threshold updated');
|
|
header('Location: /admin/inventory.php');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'bulk_adjust') {
|
|
$adjustments = $_POST['adjustments'] ?? [];
|
|
$count = 0;
|
|
foreach ($adjustments as $productId => $adj) {
|
|
$adj = intval($adj);
|
|
if ($adj != 0) {
|
|
db()->query(
|
|
"UPDATE products SET stock = GREATEST(0, stock + :adj) WHERE product_id = :id",
|
|
['adj' => $adj, 'id' => $productId]
|
|
);
|
|
$count++;
|
|
}
|
|
}
|
|
if ($count > 0) {
|
|
setFlash('success', "Adjusted stock for $count products");
|
|
}
|
|
header('Location: /admin/inventory.php');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Filters
|
|
$filter = $_GET['filter'] ?? '';
|
|
$search = $_GET['search'] ?? '';
|
|
$category = $_GET['category'] ?? '';
|
|
|
|
$where = ['1=1'];
|
|
$params = [];
|
|
|
|
if ($search) {
|
|
$where[] = '(name LIKE :search OR sku LIKE :search OR barcode LIKE :search)';
|
|
$params['search'] = '%' . $search . '%';
|
|
}
|
|
|
|
if ($category) {
|
|
$where[] = 'category = :category';
|
|
$params['category'] = $category;
|
|
}
|
|
|
|
if ($filter === 'low') {
|
|
$where[] = 'stock <= low_stock_threshold AND stock > 0';
|
|
} elseif ($filter === 'out') {
|
|
$where[] = 'stock <= 0';
|
|
} elseif ($filter === 'in') {
|
|
$where[] = 'stock > low_stock_threshold';
|
|
}
|
|
|
|
$whereClause = implode(' AND ', $where);
|
|
|
|
$products = db()->fetchAll(
|
|
"SELECT product_id, name, sku, barcode, category, stock, low_stock_threshold, price, is_active
|
|
FROM products
|
|
WHERE {$whereClause}
|
|
ORDER BY stock ASC, name ASC",
|
|
$params
|
|
);
|
|
|
|
// Get categories for filter
|
|
$categories = db()->fetchAll(
|
|
"SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category"
|
|
);
|
|
|
|
// Stats
|
|
$totalProducts = db()->count('products', 'is_active = 1');
|
|
$lowStockCount = db()->count('products', 'stock <= low_stock_threshold AND stock > 0 AND is_active = 1');
|
|
$outOfStockCount = db()->count('products', 'stock <= 0 AND is_active = 1');
|
|
$totalStock = db()->fetch("SELECT SUM(stock) as total FROM products WHERE is_active = 1")['total'] ?? 0;
|
|
$inventoryValue = db()->fetch("SELECT SUM(stock * price) as total FROM products WHERE is_active = 1")['total'] ?? 0;
|
|
?>
|
|
|
|
<div class="page-header">
|
|
<h1 class="page-title">Inventory Management</h1>
|
|
<button class="btn btn-primary" onclick="document.getElementById('bulkForm').style.display = document.getElementById('bulkForm').style.display === 'none' ? 'block' : 'none'">
|
|
<i class="fas fa-boxes"></i> Bulk Adjust
|
|
</button>
|
|
</div>
|
|
|
|
<?php if (hasFlash('success')): ?>
|
|
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon primary"><i class="fas fa-boxes"></i></div>
|
|
<div>
|
|
<div class="stat-card-value"><?= number_format($totalStock) ?></div>
|
|
<div class="stat-card-label">Total Units</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon success"><i class="fas fa-dollar-sign"></i></div>
|
|
<div>
|
|
<div class="stat-card-value"><?= formatCurrency($inventoryValue) ?></div>
|
|
<div class="stat-card-label">Inventory Value</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon warning"><i class="fas fa-exclamation-triangle"></i></div>
|
|
<div>
|
|
<div class="stat-card-value"><?= $lowStockCount ?></div>
|
|
<div class="stat-card-label">Low Stock</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon error"><i class="fas fa-times-circle"></i></div>
|
|
<div>
|
|
<div class="stat-card-value"><?= $outOfStockCount ?></div>
|
|
<div class="stat-card-label">Out of Stock</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="admin-card">
|
|
<div class="admin-card-body">
|
|
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: 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, barcode..." 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 Categories</option>
|
|
<?php foreach ($categories as $cat): ?>
|
|
<option value="<?= htmlspecialchars($cat['category']) ?>" <?= $category === $cat['category'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars(ucfirst($cat['category'])) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group mb-0">
|
|
<label class="form-label">Stock Status</label>
|
|
<select name="filter" class="form-select">
|
|
<option value="">All</option>
|
|
<option value="in" <?= $filter === 'in' ? 'selected' : '' ?>>In Stock</option>
|
|
<option value="low" <?= $filter === 'low' ? 'selected' : '' ?>>Low Stock</option>
|
|
<option value="out" <?= $filter === 'out' ? 'selected' : '' ?>>Out of Stock</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
|
<?php if ($filter || $search || $category): ?>
|
|
<a href="/admin/inventory.php" class="btn btn-secondary">Clear</a>
|
|
<?php endif; ?>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Adjustment Form (hidden by default) -->
|
|
<form method="POST" id="bulkForm" style="display: none;">
|
|
<input type="hidden" name="action" value="bulk_adjust">
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<h3 class="admin-card-title">Bulk Stock Adjustment</h3>
|
|
</div>
|
|
<div class="admin-card-body">
|
|
<p class="text-muted mb-1">Enter adjustment values for each product (positive to add, negative to subtract). Leave blank or 0 to skip.</p>
|
|
<button type="submit" class="btn btn-primary mb-1">Apply All Adjustments</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Inventory Table -->
|
|
<div class="admin-card">
|
|
<div class="admin-card-header">
|
|
<span><?= count($products) ?> products</span>
|
|
</div>
|
|
<div class="admin-card-body" style="padding: 0;">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>SKU / Barcode</th>
|
|
<th>Category</th>
|
|
<th>Stock</th>
|
|
<th>Threshold</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($products)): ?>
|
|
<tr><td colspan="7" class="text-muted" style="text-align: center; padding: 2rem;">No products found</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($products as $product): ?>
|
|
<tr>
|
|
<td>
|
|
<strong><?= htmlspecialchars($product['name']) ?></strong>
|
|
<?php if (!$product['is_active']): ?>
|
|
<span class="badge badge-error">Inactive</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($product['sku']): ?>
|
|
<code><?= htmlspecialchars($product['sku']) ?></code>
|
|
<?php endif; ?>
|
|
<?php if ($product['barcode']): ?>
|
|
<br><small class="text-muted"><?= htmlspecialchars($product['barcode']) ?></small>
|
|
<?php endif; ?>
|
|
<?php if (!$product['sku'] && !$product['barcode']): ?>
|
|
<span class="text-muted">-</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($product['category'] ?? '-') ?></td>
|
|
<td>
|
|
<span style="font-size: 1.5rem; font-weight: 700; color: <?= $product['stock'] <= 0 ? 'var(--admin-error)' : ($product['stock'] <= $product['low_stock_threshold'] ? 'var(--admin-warning)' : 'var(--admin-success)') ?>;">
|
|
<?= $product['stock'] ?>
|
|
</span>
|
|
<input type="number" name="adjustments[<?= $product['product_id'] ?>]" form="bulkForm"
|
|
class="form-input" style="width: 80px; margin-left: 0.5rem; display: none;" placeholder="+/-">
|
|
</td>
|
|
<td>
|
|
<span class="text-muted"><?= $product['low_stock_threshold'] ?></span>
|
|
</td>
|
|
<td>
|
|
<?php if ($product['stock'] <= 0): ?>
|
|
<span class="badge badge-error">Out of Stock</span>
|
|
<?php elseif ($product['stock'] <= $product['low_stock_threshold']): ?>
|
|
<span class="badge badge-warning">Low Stock</span>
|
|
<?php else: ?>
|
|
<span class="badge badge-success">In Stock</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="openAdjustModal('<?= $product['product_id'] ?>', '<?= htmlspecialchars(addslashes($product['name'])) ?>', <?= $product['stock'] ?>, <?= $product['low_stock_threshold'] ?>)" title="Adjust Stock">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<a href="/admin/product-edit.php?id=<?= $product['product_id'] ?>" class="btn btn-sm btn-secondary" title="Edit Product">
|
|
<i class="fas fa-cog"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Adjust Modal -->
|
|
<div class="modal-overlay" id="adjustModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Adjust Stock</h3>
|
|
<button type="button" class="modal-close" onclick="Modal.close('adjustModal')">×</button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="action" value="adjust">
|
|
<input type="hidden" name="product_id" id="adjustProductId">
|
|
|
|
<p><strong id="adjustProductName"></strong></p>
|
|
<p>Current Stock: <strong id="adjustCurrentStock"></strong></p>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Stock Adjustment</label>
|
|
<input type="number" name="adjustment" id="adjustmentInput" class="form-input" required placeholder="e.g., 10 or -5">
|
|
<small class="text-muted">Positive to add, negative to subtract</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Reason (optional)</label>
|
|
<input type="text" name="reason" class="form-input" placeholder="e.g., Received shipment, Damaged goods">
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="form-group mb-0">
|
|
<label class="form-label">Low Stock Threshold</label>
|
|
<input type="number" name="low_stock_threshold" id="adjustThreshold" class="form-input" min="0">
|
|
<small class="text-muted">Alert when stock falls to this level</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="Modal.close('adjustModal')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openAdjustModal(productId, name, stock, threshold) {
|
|
document.getElementById('adjustProductId').value = productId;
|
|
document.getElementById('adjustProductName').textContent = name;
|
|
document.getElementById('adjustCurrentStock').textContent = stock;
|
|
document.getElementById('adjustThreshold').value = threshold;
|
|
document.getElementById('adjustmentInput').value = '';
|
|
Modal.open('adjustModal');
|
|
}
|
|
|
|
// Toggle bulk adjustment inputs
|
|
document.getElementById('bulkForm').addEventListener('toggle', function() {
|
|
document.querySelectorAll('input[name^="adjustments"]').forEach(input => {
|
|
input.style.display = this.style.display === 'none' ? 'none' : 'inline-block';
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|