38682-vm/admin/purchases.php
2026-02-24 10:05:25 +00:00

540 lines
26 KiB
PHP

<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("purchases_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle SAVE (Add/Edit) Purchase via POST
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_purchase') {
$id = $_POST['id'] ?: null;
$supplier_id = $_POST['supplier_id'] ?: null;
$purchase_date = $_POST['purchase_date'];
$status = $_POST['status'];
$notes = $_POST['notes'];
$product_ids = $_POST['product_id'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$cost_prices = $_POST['cost_price'] ?? [];
try {
$pdo->beginTransaction();
$total_amount = 0;
foreach ($product_ids as $index => $pid) {
$total_amount += $quantities[$index] * $cost_prices[$index];
}
$purchase = null;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM purchases WHERE id = ?");
$stmt->execute([$id]);
$purchase = $stmt->fetch();
if ($purchase) {
$old_status = $purchase['status'];
$stmt = $pdo->prepare("UPDATE purchases SET supplier_id = ?, purchase_date = ?, status = ?, notes = ?, total_amount = ? WHERE id = ?");
$stmt->execute([$supplier_id, $purchase_date, $status, $notes, $total_amount, $id]);
$stmt = $pdo->prepare("SELECT * FROM purchase_items WHERE purchase_id = ?");
$stmt->execute([$id]);
$old_items = $stmt->fetchAll();
if ($old_status === 'completed') {
foreach ($old_items as $oi) {
$pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?")
->execute([$oi['quantity'], $oi['product_id']]);
}
}
$pdo->prepare("DELETE FROM purchase_items WHERE purchase_id = ?")->execute([$id]);
}
} else {
$stmt = $pdo->prepare("INSERT INTO purchases (supplier_id, purchase_date, status, notes, total_amount) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$supplier_id, $purchase_date, $status, $notes, $total_amount]);
$id = $pdo->lastInsertId();
}
foreach ($product_ids as $index => $pid) {
$qty = $quantities[$index];
$cost = $cost_prices[$index];
$total_item_price = $qty * $cost;
$stmt = $pdo->prepare("INSERT INTO purchase_items (purchase_id, product_id, quantity, cost_price, total_price) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$id, $pid, $qty, $cost, $total_item_price]);
if ($status === 'completed') {
$pdo->prepare("UPDATE products SET stock_quantity = stock_quantity + ?, cost_price = ? WHERE id = ?")
->execute([$qty, $cost, $pid]);
}
}
$pdo->commit();
$message = '<div class="alert alert-success alert-dismissible fade show" role="alert">Purchase saved successfully!<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
} catch (Exception $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger">Error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('purchases_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete purchases.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM purchases WHERE id = ?")->execute([$id]);
header("Location: purchases.php?msg=deleted");
exit;
}
}
if (isset($_GET['msg']) && $_GET['msg'] === 'deleted') {
$message = '<div class="alert alert-success alert-dismissible fade show" role="alert">Purchase record deleted successfully!<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
}
$suppliers = $pdo->query("SELECT * FROM suppliers ORDER BY name")->fetchAll();
$products = $pdo->query("SELECT id, name, cost_price FROM products ORDER BY name")->fetchAll();
$products_json = json_encode($products);
$search = $_GET['search'] ?? '';
$supplier_filter = $_GET['supplier_filter'] ?? '';
$status_filter = $_GET['status_filter'] ?? '';
$params = [];
$where = [];
$query = "SELECT p.*, s.name as supplier_name
FROM purchases p
LEFT JOIN suppliers s ON p.supplier_id = s.id";
if ($search) {
$where[] = "p.notes LIKE ?";
$params[] = "%$search%";
}
if ($supplier_filter) {
$where[] = "p.supplier_id = ?";
$params[] = $supplier_filter;
}
if ($status_filter) {
$where[] = "p.status = ?";
$params[] = $status_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY p.purchase_date DESC, p.id DESC";
$purchases_pagination = paginate_query($pdo, $query, $params);
$purchases = $purchases_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1 text-dark">Purchases Inventory</h2>
<p class="text-muted mb-0">Manage restocks, supplier invoices and inventory tracking</p>
</div>
<?php if (has_permission('purchases_add')): ?>
<button type="button" class="btn btn-primary btn-lg shadow-sm" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#purchaseModal" onclick="prepareAddPurchase()">
<i class="bi bi-plus-lg me-1"></i> New Purchase Order
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4 rounded-4">
<div class="card-body p-4">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light" placeholder="Search by notes..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<select name="supplier_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Suppliers</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= $s['id'] ?>" <?= $supplier_filter == $s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<select name="status_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Status</option>
<option value="pending" <?= $status_filter == 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="completed" <?= $status_filter == 'completed' ? 'selected' : '' ?>>Completed</option>
<option value="cancelled" <?= $status_filter == 'cancelled' ? 'selected' : '' ?>>Cancelled</option>
</select>
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 w-100 rounded-pill fw-bold">Filter Results</button>
<?php if ($search || $supplier_filter || $status_filter): ?>
<a href="purchases.php" class="btn btn-light text-muted px-3 rounded-circle d-flex align-items-center justify-content-center"><i class="bi bi-x-lg"></i></a>
<?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">Purchase Details</th>
<th>Supplier</th>
<th>Status</th>
<th>Total Amount</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($purchases as $p):
$status_badge = 'bg-secondary';
if ($p['status'] === 'completed') $status_badge = 'bg-success-subtle text-success border border-success';
if ($p['status'] === 'pending') $status_badge = 'bg-warning-subtle text-warning border border-warning';
if ($p['status'] === 'cancelled') $status_badge = 'bg-danger-subtle text-danger border border-danger';
?>
<tr>
<td class="ps-4">
<div class="fw-bold text-dark"><?= date('M d, Y', strtotime($p['purchase_date'])) ?></div>
<div class="small text-muted fw-medium">Ref: #INV-<?= str_pad($p['id'], 5, '0', STR_PAD_LEFT) ?></div>
</td>
<td>
<div class="fw-bold text-dark"><?= htmlspecialchars($p['supplier_name'] ?? 'Direct Purchase') ?></div>
</td>
<td>
<span class="badge <?= $status_badge ?> rounded-pill px-3 py-2 small fw-bold text-uppercase" style="font-size: 0.7rem;"><?= ucfirst($p['status']) ?></span>
</td>
<td>
<div class="fw-bold text-primary fs-5"><?= format_currency($p['total_amount']) ?></div>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('purchases_edit') || has_permission('purchases_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3" onclick="editPurchase(<?= $p['id'] ?>)" title="Edit/View">
<i class="bi bi-pencil-square me-1"></i> Edit
</button>
<?php endif; ?>
<?php if (has_permission('purchases_del')): ?>
<a href="?delete=<?= $p['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Are you sure you want to delete this purchase record?')" title="Delete">
<i class="bi bi-trash me-1"></i> Delete
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($purchases)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<div class="mb-2 display-6 opacity-25"><i class="bi bi-cart-x"></i></div>
<h5 class="fw-bold">No purchase records found.</h5>
<p class="small">Try different search terms or start by adding a new purchase.</p>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (!empty($purchases)): ?>
<div class="p-4 border-top bg-light">
<?php render_pagination_controls($purchases_pagination); ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Purchase Modal -->
<div class="modal fade" id="purchaseModal" tabindex="-1" aria-labelledby="purchaseModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="purchaseModalLabel">New Purchase Order</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<form method="POST" id="purchaseForm" class="p-4">
<input type="hidden" name="action" value="save_purchase">
<input type="hidden" name="id" id="purchaseId">
<div class="row g-4">
<div class="col-12">
<div class="card border-0 bg-light mb-4 rounded-3">
<div class="card-body p-4">
<h6 class="fw-bold mb-3">General Information</h6>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">SUPPLIER</label>
<select name="supplier_id" id="modal_supplier_id" class="form-select border-0 shadow-sm">
<option value="">Direct Purchase / None</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">PURCHASE DATE</label>
<input type="date" name="purchase_date" id="modal_purchase_date" class="form-control border-0 shadow-sm" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">STATUS</label>
<select name="status" id="modal_status" class="form-select border-0 shadow-sm">
<option value="pending">Pending</option>
<option value="completed">Completed (Updates Stock)</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="col-12">
<label class="form-label text-muted small fw-bold">NOTES</label>
<textarea name="notes" id="modal_notes" class="form-control border-0 shadow-sm" rows="2" placeholder="Reference No, delivery details..."></textarea>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-3">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h6 class="fw-bold mb-0">Products to Purchase</h6>
<div class="position-relative" style="width: 350px;">
<div class="input-group shadow-sm rounded-pill overflow-hidden">
<span class="input-group-text bg-white border-0 text-muted ps-3"><i class="bi bi-search"></i></span>
<input type="text" id="productSearch" class="form-control border-0" placeholder="Search products to add...">
</div>
<div id="searchResults" class="dropdown-menu w-100 shadow-sm mt-1" style="max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle" id="itemsTable">
<thead class="table-light">
<tr>
<th style="width: 40%;">PRODUCT</th>
<th style="width: 20%;">QTY</th>
<th style="width: 20%;">COST</th>
<th style="width: 15%;" class="text-end">TOTAL</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody id="itemsBody">
<!-- Items will be added here -->
</tbody>
<tfoot id="tableFooter" class="d-none">
<tr>
<td colspan="3" class="text-end fw-bold pt-4">Grand Total:</td>
<td class="text-end fw-bold pt-4 text-primary fs-5" id="grandTotal"><?= format_currency(0) ?></td>
<td></td>
</tr>
</tfoot>
</table>
<div id="noItemsMessage" class="text-center py-5 text-muted">
<div class="mb-2 display-6 opacity-25"><i class="bi bi-cart-plus"></i></div>
<div>Use the search bar above to add products to this purchase.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="purchaseForm" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">
<i class="bi bi-check-lg me-1"></i> Save Purchase
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const products = <?= $products_json ?>;
const productSearch = document.getElementById('productSearch');
const searchResults = document.getElementById('searchResults');
const itemsBody = document.getElementById('itemsBody');
const grandTotalElement = document.getElementById('grandTotal');
const noItemsMessage = document.getElementById('noItemsMessage');
const tableFooter = document.getElementById('tableFooter');
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}
function calculateTotals() {
let grandTotal = 0;
const rows = document.querySelectorAll('.item-row');
rows.forEach(row => {
const qty = parseFloat(row.querySelector('.qty-input').value) || 0;
const cost = parseFloat(row.querySelector('.cost-input').value) || 0;
const total = qty * cost;
row.querySelector('.row-total').textContent = formatCurrency(total);
grandTotal += total;
});
grandTotalElement.textContent = formatCurrency(grandTotal);
if (rows.length > 0) {
noItemsMessage.classList.add('d-none');
tableFooter.classList.remove('d-none');
} else {
noItemsMessage.classList.remove('d-none');
tableFooter.classList.add('d-none');
}
}
productSearch.addEventListener('input', function() {
const query = this.value.toLowerCase().trim();
searchResults.innerHTML = '';
if (query.length < 1) {
searchResults.classList.remove('show');
return;
}
const filtered = products.filter(p => p.name.toLowerCase().includes(query));
if (filtered.length > 0) {
filtered.forEach(p => {
const item = document.createElement('a');
item.className = 'dropdown-item py-2 border-bottom';
item.href = '#';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">${p.name}</div>
<small class="text-muted">Cost: ${formatCurrency(p.cost_price)}</small>
</div>
<i class="bi bi-plus-circle text-primary"></i>
</div>
`;
item.addEventListener('click', function(e) {
e.preventDefault();
addProductToTable(p);
productSearch.value = '';
searchResults.classList.remove('show');
});
searchResults.appendChild(item);
});
searchResults.classList.add('show');
} else {
const item = document.createElement('div');
item.className = 'dropdown-item disabled text-center py-3';
item.textContent = 'No products found';
searchResults.appendChild(item);
searchResults.classList.add('show');
}
});
document.addEventListener('click', function(e) {
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.classList.remove('show');
}
});
function addProductToTable(product, quantity = 1, costPrice = null) {
const existingRow = Array.from(document.querySelectorAll('.item-row')).find(row => row.dataset.productId == product.id);
if (existingRow && costPrice === null) {
const qtyInput = existingRow.querySelector('.qty-input');
qtyInput.value = parseInt(qtyInput.value) + 1;
calculateTotals();
return;
}
const cost = costPrice !== null ? costPrice : product.cost_price;
const row = document.createElement('tr');
row.className = 'item-row';
row.dataset.productId = product.id;
row.innerHTML = `
<td>
<div class="fw-bold">${product.name}</div>
<input type="hidden" name="product_id[]" value="${product.id}">
</td>
<td><input type="number" name="quantity[]" class="form-control qty-input border-0 bg-light" min="1" value="${quantity}" required></td>
<td><input type="number" step="0.01" name="cost_price[]" class="form-control cost-input border-0 bg-light" value="${cost}" required></td>
<td class="text-end fw-bold row-total">${formatCurrency(quantity * cost)}</td>
<td class="text-end"><button type="button" class="btn btn-link text-danger remove-item p-0 shadow-none"><i class="bi bi-trash"></i></button></td>
`;
itemsBody.appendChild(row);
calculateTotals();
}
itemsBody.addEventListener('input', function(e) {
if (e.target.classList.contains('qty-input') || e.target.classList.contains('cost-input')) {
calculateTotals();
}
});
itemsBody.addEventListener('click', function(e) {
if (e.target.closest('.remove-item')) {
e.target.closest('.item-row').remove();
calculateTotals();
}
});
window.prepareAddPurchase = function() {
document.getElementById('purchaseModalLabel').innerText = 'New Purchase Order';
document.getElementById('purchaseId').value = '';
document.getElementById('purchaseForm').reset();
document.getElementById('modal_purchase_date').value = new Date().toISOString().split('T')[0];
itemsBody.innerHTML = '';
calculateTotals();
};
window.editPurchase = function(id) {
document.getElementById('purchaseModalLabel').innerText = 'Edit Purchase #' + id;
document.getElementById('purchaseId').value = id;
itemsBody.innerHTML = '';
const modal = new bootstrap.Modal(document.getElementById('purchaseModal'));
modal.show();
fetch('../api/purchase_details.php?id=' + id)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('modal_supplier_id').value = data.purchase.supplier_id || '';
document.getElementById('modal_purchase_date').value = data.purchase.purchase_date;
document.getElementById('modal_status').value = data.purchase.status;
document.getElementById('modal_notes').value = data.purchase.notes || '';
data.items.forEach(item => {
addProductToTable({
id: item.product_id,
name: item.product_name,
cost_price: item.cost_price
}, item.quantity, item.cost_price);
});
} else {
alert('Error: ' + data.error);
modal.hide();
}
});
};
});
</script>
<style>
.dropdown-menu.show {
display: block;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.modal-xl {
max-width: 1140px;
}
</style>
<?php include 'includes/footer.php'; ?>