38348-vm/quotation_form.php
2026-02-11 01:46:33 +00:00

331 lines
16 KiB
PHP

<?php
$page_title = isset($_GET['id']) ? "Edit Quotation" : "New Quotation";
require_once 'includes/header.php';
// Auth check (Sales or Admin can manage quotations)
if (!in_array($_SESSION['role'], ['Admin', 'Sales'])) {
$_SESSION['error'] = "You do not have permission to manage quotations.";
header("Location: index.php");
exit;
}
$id = $_GET['id'] ?? null;
$quotation = null;
$items = [];
if ($id) {
$stmt = db()->prepare("SELECT * FROM quotations WHERE id = ? AND deleted_at IS NULL");
$stmt->execute([$id]);
$quotation = $stmt->fetch();
if ($quotation) {
$stmt = db()->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
$stmt->execute([$id]);
$items = $stmt->fetchAll();
} else {
header("Location: quotations.php");
exit;
}
}
// Fetch customers and products for dropdowns
$customers = db()->query("SELECT id, name FROM customers WHERE deleted_at IS NULL ORDER BY name ASC")->fetchAll();
$products = db()->query("SELECT id, name, price FROM products WHERE deleted_at IS NULL ORDER BY name ASC")->fetchAll();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validate_csrf($_POST['csrf_token'] ?? '')) {
die("CSRF token validation failed");
}
$customer_id = $_POST['customer_id'];
$issue_date = $_POST['issue_date'];
$expiry_date = $_POST['expiry_date'];
$status = $_POST['status'];
$notes = $_POST['notes'];
$subtotal = 0;
$form_items = $_POST['items'] ?? [];
$processed_items = [];
foreach ($form_items as $item) {
if (!empty($item['product_id']) && $item['quantity'] > 0) {
$total_price = $item['quantity'] * $item['unit_price'];
$subtotal += $total_price;
$processed_items[] = [
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_price' => $total_price
];
}
}
$tax_rate = 0.10; // 10% tax example
$tax_amount = $subtotal * $tax_rate;
$total_amount = $subtotal + $tax_amount;
try {
db()->beginTransaction();
if ($id) {
$stmt = db()->prepare("UPDATE quotations SET customer_id = ?, issue_date = ?, expiry_date = ?, status = ?, subtotal = ?, tax_amount = ?, total_amount = ?, notes = ? WHERE id = ?");
$stmt->execute([$customer_id, $issue_date, $expiry_date, $status, $subtotal, $tax_amount, $total_amount, $notes, $id]);
// Delete old items
db()->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$id]);
} else {
// Generate quotation number
$q_number = 'QTN-' . date('Ymd') . '-' . strtoupper(bin2hex(random_bytes(2)));
$stmt = db()->prepare("INSERT INTO quotations (quotation_number, customer_id, user_id, issue_date, expiry_date, status, subtotal, tax_amount, total_amount, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$q_number, $customer_id, $_SESSION['user_id'], $issue_date, $expiry_date, $status, $subtotal, $tax_amount, $total_amount, $notes]);
$id = db()->lastInsertId();
}
// Insert items
$item_stmt = db()->prepare("INSERT INTO quotation_items (quotation_id, product_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
foreach ($processed_items as $p_item) {
$item_stmt->execute([$id, $p_item['product_id'], $p_item['quantity'], $p_item['unit_price'], $p_item['total_price']]);
}
// Log action
$log_stmt = db()->prepare("INSERT INTO audit_logs (user_id, action, entity_type, entity_id, details) VALUES (?, ?, ?, ?, ?)");
$log_stmt->execute([$_SESSION['user_id'], $id ? 'UPDATE' : 'CREATE', 'QUOTATION', $id, "Number: " . ($quotation['quotation_number'] ?? $q_number)]);
db()->commit();
$_SESSION['success'] = "Quotation saved successfully.";
header("Location: quotations.php");
exit;
} catch (Exception $e) {
db()->rollBack();
$error = "Error saving quotation: " . $e->getMessage();
}
}
$default_issue_date = $quotation['issue_date'] ?? date('Y-m-d');
$default_expiry_date = $quotation['expiry_date'] ?? date('Y-m-d', strtotime('+30 days'));
?>
<div class="mb-4">
<a href="quotations.php" class="text-decoration-none text-muted">
<i class="bi bi-arrow-left me-1"></i> Back to Quotations
</a>
<h2 class="h3 mt-2 fw-bold"><?= $page_title ?></h2>
</div>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= e($error) ?></div>
<?php endif; ?>
<form method="POST" id="quotation-form">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<div class="row g-4">
<div class="col-lg-8">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0">General Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Customer</label>
<select name="customer_id" class="form-select" required>
<option value="">Select Customer</option>
<?php foreach ($customers as $c): ?>
<option value="<?= $c['id'] ?>" <?= ($quotation['customer_id'] ?? '') == $c['id'] ? 'selected' : '' ?>>
<?= e($c['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date</label>
<input type="date" name="issue_date" class="form-control" value="<?= e($default_issue_date) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label">Expiry Date</label>
<input type="date" name="expiry_date" class="form-control" value="<?= e($default_expiry_date) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select name="status" class="form-select" required>
<option value="Draft" <?= ($quotation['status'] ?? '') == 'Draft' ? 'selected' : '' ?>>Draft</option>
<option value="Sent" <?= ($quotation['status'] ?? '') == 'Sent' ? 'selected' : '' ?>>Sent</option>
<option value="Approved" <?= ($quotation['status'] ?? '') == 'Approved' ? 'selected' : '' ?>>Approved</option>
<option value="Rejected" <?= ($quotation['status'] ?? '') == 'Rejected' ? 'selected' : '' ?>>Rejected</option>
</select>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent py-3 d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Items</h5>
<button type="button" class="btn btn-sm btn-outline-primary" id="add-item">
<i class="bi bi-plus-lg me-1"></i> Add Item
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="items-table">
<thead class="bg-light">
<tr>
<th class="ps-4" style="width: 40%;">Product/Service</th>
<th style="width: 15%;">Qty</th>
<th style="width: 20%;">Unit Price</th>
<th style="width: 20%;">Total</th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody>
<?php
$items_to_render = !empty($items) ? $items : [['product_id' => '', 'quantity' => 1, 'unit_price' => 0]];
foreach ($items_to_render as $idx => $item):
?>
<tr class="item-row">
<td class="ps-4">
<select name="items[<?= $idx ?>][product_id]" class="form-select product-select" required>
<option value="" data-price="0">Select Product</option>
<?php foreach ($products as $p): ?>
<option value="<?= $p['id'] ?>" data-price="<?= $p['price'] ?>" <?= $item['product_id'] == $p['id'] ? 'selected' : '' ?>>
<?= e($p['name']) ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td>
<input type="number" name="items[<?= $idx ?>][quantity]" class="form-control qty-input" value="<?= $item['quantity'] ?>" step="0.01" min="0.01" required>
</td>
<td>
<div class="input-group">
<span class="input-group-text bg-light">$</span>
<input type="number" name="items[<?= $idx ?>][unit_price]" class="form-control price-input" value="<?= $item['unit_price'] ?>" step="0.01" min="0" required>
</div>
</td>
<td class="text-end fw-medium row-total">
<?= format_currency(($item['quantity'] * $item['unit_price'])) ?>
</td>
<td class="pe-4">
<button type="button" class="btn btn-link text-danger p-0 remove-item">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0">Notes</h5>
</div>
<div class="card-body">
<textarea name="notes" class="form-control" rows="4" placeholder="Additional information..."><?= e($quotation['notes'] ?? '') ?></textarea>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span class="fw-medium" id="subtotal"><?= format_currency($quotation['subtotal'] ?? 0) ?></span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Tax (10%)</span>
<span class="fw-medium" id="tax-amount"><?= format_currency($quotation['tax_amount'] ?? 0) ?></span>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center">
<span class="h6 mb-0 fw-bold">Total</span>
<span class="h5 mb-0 fw-bold text-primary" id="total-amount"><?= format_currency($quotation['total_amount'] ?? 0) ?></span>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-3">
<i class="bi bi-check2-circle me-2"></i> Save Quotation
</button>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('items-table').getElementsByTagName('tbody')[0];
const addButton = document.getElementById('add-item');
let rowIdx = <?= count($items_to_render) ?>;
function formatCurrency(amount) {
return '$' + parseFloat(amount).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
function calculateTotals() {
let subtotal = 0;
document.querySelectorAll('.item-row').forEach(row => {
const qty = parseFloat(row.querySelector('.qty-input').value) || 0;
const price = parseFloat(row.querySelector('.price-input').value) || 0;
const total = qty * price;
subtotal += total;
row.querySelector('.row-total').textContent = formatCurrency(total);
});
const taxRate = 0.10;
const taxAmount = subtotal * taxRate;
const totalAmount = subtotal + taxAmount;
document.getElementById('subtotal').textContent = formatCurrency(subtotal);
document.getElementById('tax-amount').textContent = formatCurrency(taxAmount);
document.getElementById('total-amount').textContent = formatCurrency(totalAmount);
}
addButton.addEventListener('click', function() {
const newRow = table.rows[0].cloneNode(true);
newRow.querySelectorAll('input').forEach(input => {
input.value = input.classList.contains('qty-input') ? '1' : '0';
const name = input.getAttribute('name');
input.setAttribute('name', name.replace(/\[\d+\]/, '[' + rowIdx + ']'));
});
newRow.querySelectorAll('select').forEach(select => {
select.value = '';
const name = select.getAttribute('name');
select.setAttribute('name', name.replace(/\[\d+\]/, '[' + rowIdx + ']'));
});
newRow.querySelector('.row-total').textContent = '$0.00';
table.appendChild(newRow);
rowIdx++;
attachListeners(newRow);
});
function attachListeners(row) {
row.querySelector('.remove-item').addEventListener('click', function() {
if (table.rows.length > 1) {
row.remove();
calculateTotals();
}
});
row.querySelector('.product-select').addEventListener('change', function() {
const price = this.options[this.selectedIndex].getAttribute('data-price');
if (price) {
row.querySelector('.price-input').value = price;
calculateTotals();
}
});
row.querySelector('.qty-input').addEventListener('input', calculateTotals);
row.querySelector('.price-input').addEventListener('input', calculateTotals);
}
document.querySelectorAll('.item-row').forEach(attachListeners);
calculateTotals();
});
</script>
<?php require_once 'includes/footer.php'; ?>