purchase update
This commit is contained in:
parent
6cbe1c3306
commit
e2fb4c84bf
@ -401,7 +401,7 @@ function can_view($module) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$financialsGroup = ['expenses.php', 'expense_categories.php', 'expense_category_edit.php', 'purchases.php', 'purchase_edit.php'];
|
$financialsGroup = ['expenses.php', 'expense_categories.php', 'expense_category_edit.php', 'purchases.php'];
|
||||||
$canViewFinancialsGroup = can_view('expenses') || can_view('expense_categories') || can_view('purchases');
|
$canViewFinancialsGroup = can_view('expenses') || can_view('expense_categories') || can_view('purchases');
|
||||||
if ($canViewFinancialsGroup):
|
if ($canViewFinancialsGroup):
|
||||||
?>
|
?>
|
||||||
@ -415,7 +415,7 @@ function can_view($module) {
|
|||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<?php if (can_view('purchases')): ?>
|
<?php if (can_view('purchases')): ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('purchases.php') || isActive('purchase_edit.php') ? 'active' : '' ?>" href="purchases.php">
|
<a class="nav-link <?= isActive('purchases.php') ? 'active' : '' ?>" href="purchases.php">
|
||||||
<i class="bi bi-cart-plus me-2"></i> Purchases
|
<i class="bi bi-cart-plus me-2"></i> Purchases
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -589,7 +589,7 @@ function can_view($module) {
|
|||||||
<li><a class="dropdown-item" href="profile.php"><i class="bi bi-person me-2"></i> My Profile</a></li>
|
<li><a class="dropdown-item" href="profile.php"><i class="bi bi-person me-2"></i> My Profile</a></li>
|
||||||
<li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> Company Settings</a></li>
|
<li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> Company Settings</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item text-danger" href="/logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
|
<li><a class="dropdown-item text-danger" href="<?= get_base_url() ?>logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,17 +7,21 @@ $pdo = db();
|
|||||||
$currentUser = get_logged_user();
|
$currentUser = get_logged_user();
|
||||||
$id = $currentUser['id'];
|
$id = $currentUser['id'];
|
||||||
|
|
||||||
// Always fetch fresh data from DB
|
// Helper for fresh data
|
||||||
$stmt = $pdo->prepare("SELECT u.*, g.name as group_name, g.permissions
|
function fetch_user_data($pdo, $id) {
|
||||||
FROM users u
|
$stmt = $pdo->prepare("SELECT u.*, g.name as group_name, g.permissions
|
||||||
LEFT JOIN user_groups g ON u.group_id = g.id
|
FROM users u
|
||||||
WHERE u.id = ?");
|
LEFT JOIN user_groups g ON u.group_id = g.id
|
||||||
$stmt->execute([$id]);
|
WHERE u.id = ?");
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = fetch_user_data($pdo, $id);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
logout_user();
|
logout_user();
|
||||||
header('Location: /login.php');
|
header('Location: ' . get_base_url() . 'login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,12 +29,13 @@ $message = '';
|
|||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$full_name = $_POST['full_name'];
|
$full_name = $_POST['full_name'];
|
||||||
|
$full_name_ar = $_POST['full_name_ar'] ?? '';
|
||||||
$email = $_POST['email'];
|
$email = $_POST['email'];
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
try {
|
try {
|
||||||
$sql = "UPDATE users SET full_name = ?, email = ? WHERE id = ?";
|
$sql = "UPDATE users SET full_name = ?, full_name_ar = ?, email = ? WHERE id = ?";
|
||||||
$params = [$full_name, $email, $id];
|
$params = [$full_name, $full_name_ar, $email, $id];
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
@ -73,13 +78,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$message = '<div class="alert alert-success">Profile updated successfully!</div>';
|
$message = '<div class="alert alert-success">Profile updated successfully!</div>';
|
||||||
|
|
||||||
// Refresh user data and update session
|
// Refresh user data and update session
|
||||||
$stmt->execute([$id]);
|
$user = fetch_user_data($pdo, $id);
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
$_SESSION['user'] = $user;
|
$_SESSION['user'] = $user;
|
||||||
unset($_SESSION['user']['password']);
|
unset($_SESSION['user']['password']);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
$message = '<div class="alert alert-danger">Error updating profile: ' . $e->getMessage() . '</div>';
|
$message = '<div class="alert alert-danger">Error updating profile: ' . $e->getMessage() . '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,24 +107,38 @@ include 'includes/header.php';
|
|||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small fw-bold text-muted">FULL NAME</label>
|
<label class="form-label small fw-bold text-muted">FULL NAME (ENGLISH)</label>
|
||||||
<input type="text" name="full_name" class="form-control" value="<?= htmlspecialchars($user['full_name']) ?>" required>
|
<input type="text" name="full_name" id="full_name" class="form-control" value="<?= htmlspecialchars($user['full_name'] ?? '') ?>" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small fw-bold text-muted">USERNAME (READ-ONLY)</label>
|
<label class="form-label small fw-bold text-muted">FULL NAME (ARABIC)</label>
|
||||||
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['username']) ?>" readonly>
|
<div class="input-group">
|
||||||
|
<input type="text" name="full_name_ar" id="full_name_ar" class="form-control" value="<?= htmlspecialchars($user['full_name_ar'] ?? '') ?>" dir="rtl">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="translateName(event)">
|
||||||
|
<i class="bi bi-translate text-primary"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small fw-bold text-muted">EMAIL</label>
|
<label class="form-label small fw-bold text-muted">USERNAME (READ-ONLY)</label>
|
||||||
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email']) ?>" required>
|
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['username'] ?? '') ?>" readonly>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold text-muted">EMAIL</label>
|
||||||
|
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small fw-bold text-muted">ROLE / GROUP</label>
|
<label class="form-label small fw-bold text-muted">ROLE / GROUP</label>
|
||||||
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['group_name']) ?>" readonly>
|
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['group_name'] ?? '') ?>" readonly>
|
||||||
<label class="form-label small fw-bold text-muted mt-3">EMPLOYEE / BIOMETRIC ID</label>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold text-muted">EMPLOYEE / BIOMETRIC ID</label>
|
||||||
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['employee_id'] ?? 'Not assigned') ?>" readonly>
|
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['employee_id'] ?? 'Not assigned') ?>" readonly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,7 +151,7 @@ include 'includes/header.php';
|
|||||||
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm" style="width: 100px; height: 100px; object-fit: cover;">
|
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm" style="width: 100px; height: 100px; object-fit: cover;">
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="bg-primary bg-gradient text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 100px; height: 100px; font-weight: 700; font-size: 2rem;">
|
<div class="bg-primary bg-gradient text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 100px; height: 100px; font-weight: 700; font-size: 2rem;">
|
||||||
<?= strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
|
<?= strtoupper(substr(($user['full_name'] ?? $user['username'] ?? 'U'), 0, 1)) ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
@ -165,19 +185,19 @@ include 'includes/header.php';
|
|||||||
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm border border-3 border-white mx-auto" style="width: 120px; height: 120px; object-fit: cover;">
|
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm border border-3 border-white mx-auto" style="width: 120px; height: 120px; object-fit: cover;">
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center mx-auto shadow-sm" style="width:120px;height:120px; font-weight:700; font-size:3rem;">
|
<div class="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center mx-auto shadow-sm" style="width:120px;height:120px; font-weight:700; font-size:3rem;">
|
||||||
<?= strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
|
<?= strtoupper(substr(($user['full_name'] ?? $user['username'] ?? 'U'), 0, 1)) ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="fw-bold mb-1"><?= htmlspecialchars($user['full_name']) ?></h4>
|
<h4 class="fw-bold mb-1"><?= htmlspecialchars($user['full_name'] ?? '') ?></h4>
|
||||||
<div class="small opacity-75 mb-3">@<?= htmlspecialchars($user['username']) ?> • <?= htmlspecialchars($user['group_name']) ?></div>
|
<div class="small opacity-75 mb-3">@<?= htmlspecialchars($user['username'] ?? '') ?> • <?= htmlspecialchars($user['group_name'] ?? '') ?></div>
|
||||||
|
|
||||||
<div class="badge bg-white text-primary rounded-pill px-3 py-2 mb-3">
|
<div class="badge bg-white text-primary rounded-pill px-3 py-2 mb-3">
|
||||||
Active Account
|
Active Account
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="small opacity-75">
|
<div class="small opacity-75">
|
||||||
Member since <?= date('F d, Y', strtotime($user['created_at'])) ?>
|
Member since <?= date('F d, Y', strtotime($user['created_at'] ?? 'now')) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -195,4 +215,35 @@ include 'includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function translateName(event) {
|
||||||
|
const fullName = document.getElementById('full_name').value;
|
||||||
|
if (!fullName) return;
|
||||||
|
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
icon.className = 'spinner-border spinner-border-sm text-primary';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch('../api/translate.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: fullName,
|
||||||
|
target_lang: 'Arabic'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('full_name_ar').value = data.translated_text;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
icon.className = 'bi bi-translate text-primary';
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
@ -1,352 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . "/../includes/functions.php";
|
|
||||||
require_permission("purchases_add");
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
$message = '';
|
|
||||||
$purchase = null;
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM purchases WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$purchase = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!$purchase) {
|
|
||||||
header("Location: purchases.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT pi.*, p.name as product_name FROM purchase_items pi JOIN products p ON pi.product_id = p.id WHERE pi.purchase_id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$items = $stmt->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$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];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
$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();
|
|
||||||
header("Location: purchases.php?msg=success");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$message = '<div class="alert alert-danger">Error: ' . $e->getMessage() . '</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);
|
|
||||||
|
|
||||||
include 'includes/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="mb-4 d-flex align-items-center justify-content-between">
|
|
||||||
<div>
|
|
||||||
<a href="purchases.php" class="text-decoration-none text-muted mb-2 d-inline-block small"><i class="bi bi-arrow-left"></i> Back to Purchases</a>
|
|
||||||
<h2 class="fw-bold mb-0 text-dark"><?= $id ? 'Edit Purchase #'.$id : 'New Purchase' ?></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?= $message ?>
|
|
||||||
|
|
||||||
<form method="POST" id="purchaseForm">
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<h5 class="fw-bold mb-4">Purchase Details</h5>
|
|
||||||
<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" class="form-select">
|
|
||||||
<option value="">Direct Purchase / None</option>
|
|
||||||
<?php foreach ($suppliers as $s): ?>
|
|
||||||
<option value="<?= $s['id'] ?>" <?= ($purchase && $purchase['supplier_id'] == $s['id']) ? 'selected' : '' ?>><?= 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" class="form-control" value="<?= $purchase['purchase_date'] ?? 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" class="form-select">
|
|
||||||
<option value="pending" <?= ($purchase && $purchase['status'] == 'pending') ? 'selected' : '' ?>>Pending</option>
|
|
||||||
<option value="completed" <?= ($purchase && $purchase['status'] == 'completed') ? 'selected' : '' ?>>Completed (Updates Stock)</option>
|
|
||||||
<option value="cancelled" <?= ($purchase && $purchase['status'] == 'cancelled') ? 'selected' : '' ?>>Cancelled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label text-muted small fw-bold">NOTES</label>
|
|
||||||
<textarea name="notes" class="form-control" rows="2" placeholder="Reference No, delivery details..."><?= htmlspecialchars($purchase['notes'] ?? '') ?></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h5 class="fw-bold mb-0">Products to Purchase</h5>
|
|
||||||
<div class="position-relative" style="width: 350px;">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-search"></i></span>
|
|
||||||
<input type="text" id="productSearch" class="form-control border-start-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: 45%;">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">
|
|
||||||
<?php if (!empty($items)): ?>
|
|
||||||
<?php foreach ($items as $item): ?>
|
|
||||||
<tr class="item-row" data-product-id="<?= $item['product_id'] ?>">
|
|
||||||
<td>
|
|
||||||
<div class="fw-bold"><?= htmlspecialchars($item['product_name']) ?></div>
|
|
||||||
<input type="hidden" name="product_id[]" value="<?= $item['product_id'] ?>">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" name="quantity[]" class="form-control qty-input" min="1" value="<?= $item['quantity'] ?>" required></td>
|
|
||||||
<td><input type="number" step="0.01" name="cost_price[]" class="form-control cost-input" value="<?= $item['cost_price'] ?>" required></td>
|
|
||||||
<td class="text-end fw-bold row-total"><?= format_currency($item['total_price']) ?></td>
|
|
||||||
<td class="text-end"><button type="button" class="btn btn-link text-danger remove-item p-0"><i class="bi bi-trash"></i></button></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
<tfoot id="tableFooter" class="<?= empty($items) ? '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($purchase['total_amount'] ?? 0) ?></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div id="noItemsMessage" class="text-center py-5 text-muted <?= !empty($items) ? 'd-none' : '' ?>">
|
|
||||||
<div class="mb-2 display-6"><i class="bi bi-search"></i></div>
|
|
||||||
<div>Use the search bar above to add products to this purchase.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 text-end">
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg px-5 shadow-sm">
|
|
||||||
<i class="bi bi-check-lg me-1"></i> Save Purchase
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close search results on click outside
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
|
|
||||||
searchResults.classList.remove('show');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function addProductToTable(product) {
|
|
||||||
// Check if already in table
|
|
||||||
const existingRow = Array.from(document.querySelectorAll('.item-row')).find(row => row.dataset.productId == product.id);
|
|
||||||
if (existingRow) {
|
|
||||||
const qtyInput = existingRow.querySelector('.qty-input');
|
|
||||||
qtyInput.value = parseInt(qtyInput.value) + 1;
|
|
||||||
calculateTotals();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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" min="1" value="1" required></td>
|
|
||||||
<td><input type="number" step="0.01" name="cost_price[]" class="form-control cost-input" value="${product.cost_price}" required></td>
|
|
||||||
<td class="text-end fw-bold row-total">${format_currency(product.cost_price)}</td>
|
|
||||||
<td class="text-end"><button type="button" class="btn btn-link text-danger remove-item p-0"><i class="bi bi-trash"></i></button></td>
|
|
||||||
`;
|
|
||||||
itemsBody.appendChild(row);
|
|
||||||
calculateTotals();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to call from JS
|
|
||||||
function format_currency(amount) {
|
|
||||||
return formatCurrency(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dropdown-menu.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.form-control:focus, .form-select:focus {
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.05);
|
|
||||||
border-color: #0d6efd;
|
|
||||||
}
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
|
||||||
@ -6,18 +6,95 @@ $pdo = db();
|
|||||||
|
|
||||||
$message = '';
|
$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 (isset($_GET['delete'])) {
|
||||||
if (!has_permission('purchases_del')) {
|
if (!has_permission('purchases_del')) {
|
||||||
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete purchases.</div>';
|
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete purchases.</div>';
|
||||||
} else {
|
} else {
|
||||||
$id = $_GET['delete'];
|
$id = $_GET['delete'];
|
||||||
$pdo->prepare("DELETE FROM purchases WHERE id = ?")->execute([$id]);
|
$pdo->prepare("DELETE FROM purchases WHERE id = ?")->execute([$id]);
|
||||||
header("Location: purchases.php");
|
header("Location: purchases.php?msg=deleted");
|
||||||
exit;
|
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();
|
$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'] ?? '';
|
$search = $_GET['search'] ?? '';
|
||||||
$supplier_filter = $_GET['supplier_filter'] ?? '';
|
$supplier_filter = $_GET['supplier_filter'] ?? '';
|
||||||
@ -63,9 +140,9 @@ include 'includes/header.php';
|
|||||||
<p class="text-muted mb-0">Manage restocks, supplier invoices and inventory tracking</p>
|
<p class="text-muted mb-0">Manage restocks, supplier invoices and inventory tracking</p>
|
||||||
</div>
|
</div>
|
||||||
<?php if (has_permission('purchases_add')): ?>
|
<?php if (has_permission('purchases_add')): ?>
|
||||||
<a href="purchase_edit.php" class="btn btn-primary btn-lg shadow-sm" style="border-radius: 12px;">
|
<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
|
<i class="bi bi-plus-lg me-1"></i> New Purchase Order
|
||||||
</a>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -143,9 +220,9 @@ include 'includes/header.php';
|
|||||||
<td class="text-end pe-4">
|
<td class="text-end pe-4">
|
||||||
<div class="d-inline-flex gap-2">
|
<div class="d-inline-flex gap-2">
|
||||||
<?php if (has_permission('purchases_edit') || has_permission('purchases_add')): ?>
|
<?php if (has_permission('purchases_edit') || has_permission('purchases_add')): ?>
|
||||||
<a href="purchase_edit.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3" title="Edit/View">
|
<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
|
<i class="bi bi-pencil-square me-1"></i> Edit
|
||||||
</a>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (has_permission('purchases_del')): ?>
|
<?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">
|
<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">
|
||||||
@ -176,4 +253,288 @@ include 'includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<!-- 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'; ?>
|
||||||
31
api/purchase_details.php
Normal file
31
api/purchase_details.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . "/../includes/functions.php";
|
||||||
|
require_permission("purchases_view");
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM purchases WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$purchase = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$purchase) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Purchase not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT pi.*, p.name as product_name FROM purchase_items pi JOIN products p ON pi.product_id = p.id WHERE pi.purchase_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'purchase' => $purchase,
|
||||||
|
'items' => $items
|
||||||
|
]);
|
||||||
BIN
assets/images/users/user_1_699d76ecd9c95.jpg
Normal file
BIN
assets/images/users/user_1_699d76ecd9c95.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -264,7 +264,7 @@ function get_logged_user() {
|
|||||||
|
|
||||||
function require_login() {
|
function require_login() {
|
||||||
if (!get_logged_user()) {
|
if (!get_logged_user()) {
|
||||||
header('Location: /login.php');
|
header('Location: ' . get_base_url() . 'login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
login.php
10
login.php
@ -4,9 +4,11 @@ require_once __DIR__ . '/includes/functions.php';
|
|||||||
|
|
||||||
init_session();
|
init_session();
|
||||||
|
|
||||||
|
$baseUrl = get_base_url();
|
||||||
|
|
||||||
// Redirect if already logged in
|
// Redirect if already logged in
|
||||||
if (get_logged_user()) {
|
if (get_logged_user()) {
|
||||||
header('Location: /admin/index.php');
|
header('Location: ' . $baseUrl . 'admin/index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
if (login_user($username, $password)) {
|
if (login_user($username, $password)) {
|
||||||
header('Location: /admin/index.php');
|
header('Location: ' . $baseUrl . 'admin/index.php');
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$error = 'Invalid username or password.';
|
$error = 'Invalid username or password.';
|
||||||
@ -34,7 +36,7 @@ $settings = get_company_settings();
|
|||||||
<title>Login - <?= htmlspecialchars($settings['company_name']) ?></title>
|
<title>Login - <?= htmlspecialchars($settings['company_name']) ?></title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
<link rel="stylesheet" href="/assets/css/custom.css">
|
<link rel="stylesheet" href="<?= $baseUrl ?>assets/css/custom.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
@ -85,4 +87,4 @@ $settings = get_company_settings();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/includes/functions.php';
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
logout_user();
|
logout_user();
|
||||||
header('Location: /login.php');
|
header('Location: ' . get_base_url() . 'login.php');
|
||||||
exit;
|
exit;
|
||||||
14
test_url.php
14
test_url.php
@ -1,11 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
$protocol = "http://";
|
require_once __DIR__ . '/db/config.php';
|
||||||
$host = "localhost";
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
$_SERVER['PHP_SELF'] = "/admin/tables.php";
|
|
||||||
try {
|
|
||||||
$baseUrl = $protocol . $host . rtrim(dirname($_SERVER['PHP_SELF'], 2), '/\') . '/qorder.php';
|
|
||||||
echo "Base URL: " . $baseUrl . "\n";
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
echo "Script Name: " . $_SERVER['SCRIPT_NAME'] . "\n";
|
||||||
|
echo "Dirname: " . dirname($_SERVER['SCRIPT_NAME']) . "\n";
|
||||||
|
echo "Base URL: " . get_base_url() . "\n";
|
||||||
Loading…
x
Reference in New Issue
Block a user