add quotation
This commit is contained in:
parent
d6e5b04b82
commit
07c5256175
23
db/migrations/20260216_add_quotations.sql
Normal file
23
db/migrations/20260216_add_quotations.sql
Normal file
@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS quotations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_id INT,
|
||||
quotation_date DATE NOT NULL,
|
||||
valid_until DATE,
|
||||
status ENUM('pending', 'converted', 'expired', 'cancelled') DEFAULT 'pending',
|
||||
total_amount DECIMAL(15,3) DEFAULT 0.000,
|
||||
vat_amount DECIMAL(15,3) DEFAULT 0.000,
|
||||
total_with_vat DECIMAL(15,3) DEFAULT 0.000,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS quotation_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
quotation_id INT NOT NULL,
|
||||
item_id INT NOT NULL,
|
||||
quantity DECIMAL(15,2) NOT NULL,
|
||||
unit_price DECIMAL(15,3) DEFAULT 0.000,
|
||||
total_price DECIMAL(15,3) DEFAULT 0.000,
|
||||
FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (item_id) REFERENCES stock_items(id)
|
||||
);
|
||||
638
index.php
638
index.php
@ -721,6 +721,149 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['add_quotation'])) {
|
||||
$customer_id = $_POST['customer_id'] ?: null;
|
||||
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
||||
$valid_until = $_POST['valid_until'] ?: null;
|
||||
$item_ids = $_POST['item_ids'] ?? [];
|
||||
$quantities = $_POST['quantities'] ?? [];
|
||||
$prices = $_POST['prices'] ?? [];
|
||||
|
||||
if (!empty($item_ids)) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$subtotal = 0;
|
||||
$total_vat = 0;
|
||||
$items_data = [];
|
||||
foreach ($item_ids as $index => $item_id) {
|
||||
$qty = (float)$quantities[$index];
|
||||
$price = (float)$prices[$index];
|
||||
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||
$stmtVat->execute([$item_id]);
|
||||
$vat_rate = (float)$stmtVat->fetchColumn();
|
||||
$line_total = $qty * $price;
|
||||
$line_vat = $line_total * ($vat_rate / 100);
|
||||
$subtotal += $line_total;
|
||||
$total_vat += $line_vat;
|
||||
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
||||
}
|
||||
$total_with_vat = $subtotal + $total_vat;
|
||||
$stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$customer_id, $quotation_date, $valid_until, $subtotal, $total_vat, $total_with_vat]);
|
||||
$quotation_id = $db->lastInsertId();
|
||||
foreach ($items_data as $item) {
|
||||
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
||||
}
|
||||
$db->commit();
|
||||
$message = "Quotation #$quotation_id created successfully!";
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$message = "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_quotation'])) {
|
||||
$quotation_id = (int)$_POST['quotation_id'];
|
||||
$customer_id = $_POST['customer_id'] ?: null;
|
||||
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
||||
$valid_until = $_POST['valid_until'] ?: null;
|
||||
$status = $_POST['status'] ?? 'pending';
|
||||
$item_ids = $_POST['item_ids'] ?? [];
|
||||
$quantities = $_POST['quantities'] ?? [];
|
||||
$prices = $_POST['prices'] ?? [];
|
||||
|
||||
if ($quotation_id && !empty($item_ids)) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?");
|
||||
$stmt->execute([$quotation_id]);
|
||||
$subtotal = 0;
|
||||
$total_vat = 0;
|
||||
$items_data = [];
|
||||
foreach ($item_ids as $index => $item_id) {
|
||||
$qty = (float)$quantities[$index];
|
||||
$price = (float)$prices[$index];
|
||||
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||
$stmtVat->execute([$item_id]);
|
||||
$vat_rate = (float)$stmtVat->fetchColumn();
|
||||
$line_total = $qty * $price;
|
||||
$line_vat = $line_total * ($vat_rate / 100);
|
||||
$subtotal += $line_total;
|
||||
$total_vat += $line_vat;
|
||||
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
||||
}
|
||||
$total_with_vat = $subtotal + $total_vat;
|
||||
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
|
||||
$stmt->execute([$customer_id, $quotation_date, $valid_until, $status, $subtotal, $total_vat, $total_with_vat, $quotation_id]);
|
||||
foreach ($items_data as $item) {
|
||||
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
||||
}
|
||||
$db->commit();
|
||||
$message = "Quotation #$quotation_id updated successfully!";
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$message = "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['delete_quotation'])) {
|
||||
$id = (int)$_POST['id'];
|
||||
if ($id) {
|
||||
$stmt = db()->prepare("DELETE FROM quotations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$message = "Quotation deleted successfully!";
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['convert_to_invoice'])) {
|
||||
$quotation_id = (int)$_POST['quotation_id'];
|
||||
if ($quotation_id) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
|
||||
$stmt->execute([$quotation_id]);
|
||||
$q = $stmt->fetch();
|
||||
if (!$q) throw new Exception("Quotation not found");
|
||||
if ($q['status'] === 'converted') throw new Exception("Quotation already converted");
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
|
||||
$stmt->execute([$quotation_id]);
|
||||
$items = $stmt->fetchAll();
|
||||
|
||||
// Create Invoice
|
||||
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, CURDATE(), 'sale', 'unpaid', ?, ?, ?, 0)");
|
||||
$stmt->execute([$q['customer_id'], $q['total_amount'], $q['vat_amount'], $q['total_with_vat']]);
|
||||
$invoice_id = $db->lastInsertId();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$invoice_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['total_price']]);
|
||||
|
||||
// Update stock
|
||||
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
||||
$stmt->execute([$item['quantity'], $item['item_id']]);
|
||||
}
|
||||
|
||||
// Update quotation status
|
||||
$stmt = $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?");
|
||||
$stmt->execute([$quotation_id]);
|
||||
|
||||
$db->commit();
|
||||
$message = "Quotation converted to Invoice #$invoice_id successfully!";
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$message = "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['add_payment_method'])) {
|
||||
$name_en = $_POST['name_en'] ?? '';
|
||||
$name_ar = $_POST['name_ar'] ?? '';
|
||||
@ -961,6 +1104,37 @@ switch ($page) {
|
||||
$stmt->execute($params);
|
||||
$data['items'] = $stmt->fetchAll();
|
||||
break;
|
||||
case 'quotations':
|
||||
$where = ["1=1"];
|
||||
$params = [];
|
||||
if (!empty($_GET['search'])) {
|
||||
$where[] = "(q.id LIKE ? OR c.name LIKE ?)";
|
||||
$params[] = "%{$_GET['search']}%";
|
||||
$params[] = "%{$_GET['search']}%";
|
||||
}
|
||||
if (!empty($_GET['customer_id'])) {
|
||||
$where[] = "q.customer_id = ?";
|
||||
$params[] = $_GET['customer_id'];
|
||||
}
|
||||
if (!empty($_GET['start_date'])) {
|
||||
$where[] = "q.quotation_date >= ?";
|
||||
$params[] = $_GET['start_date'];
|
||||
}
|
||||
if (!empty($_GET['end_date'])) {
|
||||
$where[] = "q.quotation_date <= ?";
|
||||
$params[] = $_GET['end_date'];
|
||||
}
|
||||
$whereSql = implode(" AND ", $where);
|
||||
$stmt = db()->prepare("SELECT q.*, c.name as customer_name
|
||||
FROM quotations q
|
||||
LEFT JOIN customers c ON q.customer_id = c.id
|
||||
WHERE $whereSql
|
||||
ORDER BY q.id DESC");
|
||||
$stmt->execute($params);
|
||||
$data['quotations'] = $stmt->fetchAll();
|
||||
$data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items ORDER BY name_en ASC")->fetchAll();
|
||||
$data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll();
|
||||
break;
|
||||
case 'payment_methods':
|
||||
$data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll();
|
||||
break;
|
||||
@ -1110,7 +1284,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<span data-en="Operations" data-ar="العمليات">Operations</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</div>
|
||||
<div class="collapse <?= in_array($page, ['sales', 'purchases', 'pos']) ? 'show' : '' ?>" id="ops-collapse">
|
||||
<div class="collapse <?= in_array($page, ['sales', 'purchases', 'pos', 'quotations']) ? 'show' : '' ?>" id="ops-collapse">
|
||||
<a href="index.php?page=pos" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'pos' ? 'active' : '' ?>">
|
||||
<i class="bi bi-display"></i> <span data-en="Point of Sale" data-ar="نقطة البيع">Point of Sale</span>
|
||||
</a>
|
||||
@ -1120,6 +1294,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<a href="index.php?page=purchases" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchases' ? 'active' : '' ?>">
|
||||
<i class="bi bi-bag"></i> <span data-en="Purchase Tax Invoices" data-ar="فواتير المشتريات الضريبية">Purchase Tax Invoices</span>
|
||||
</a>
|
||||
<a href="index.php?page=quotations" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'quotations' ? 'active' : '' ?>">
|
||||
<i class="bi bi-file-earmark-text"></i> <span data-en="Quotations" data-ar="العروض">Quotations</span>
|
||||
</a>
|
||||
<a href="#" class="nav-link">
|
||||
<i class="bi bi-wallet2"></i> <span data-en="Expenses" data-ar="المصروفات">Expenses</span>
|
||||
</a>
|
||||
@ -1196,6 +1373,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
$titles = [
|
||||
'dashboard' => ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'],
|
||||
'pos' => ['en' => 'Point of Sale', 'ar' => 'نقطة البيع'],
|
||||
'quotations' => ['en' => 'Quotations', 'ar' => 'العروض'],
|
||||
'customers' => ['en' => 'Customers', 'ar' => 'العملاء'],
|
||||
'suppliers' => ['en' => 'Suppliers', 'ar' => 'الموردون'],
|
||||
'categories' => ['en' => 'Stock Categories', 'ar' => 'فئات المخزون'],
|
||||
@ -2528,6 +2706,109 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php elseif ($page === 'quotations'): ?>
|
||||
<div class="card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="m-0" data-en="Quotations" data-ar="عروض الأسعار">Quotations</h5>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addQuotationModal">
|
||||
<i class="bi bi-plus-lg"></i> <span data-en="Create New Quotation" data-ar="إنشاء عرض سعر جديد">Create New Quotation</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="bg-light p-3 rounded mb-4">
|
||||
<form method="GET" class="row g-3">
|
||||
<input type="hidden" name="page" value="quotations">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
||||
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Quot # or Name...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
||||
<select name="customer_id" class="form-select form-select-sm">
|
||||
<option value="" data-en="All" data-ar="الكل">All</option>
|
||||
<?php foreach ($data['customers_list'] as $c): ?>
|
||||
<option value="<?= $c['id'] ?>" <?= (($_GET['customer_id'] ?? '') == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small fw-bold" data-en="Start Date" data-ar="من تاريخ">Start Date</label>
|
||||
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small fw-bold" data-en="End Date" data-ar="إلى تاريخ">End Date</label>
|
||||
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end gap-1">
|
||||
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
||||
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
||||
</button>
|
||||
<a href="index.php?page=quotations" class="btn btn-outline-secondary btn-sm flex-grow-1">
|
||||
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-en="Quotation #" data-ar="رقم العرض">Quotation #</th>
|
||||
<th data-en="Date" data-ar="التاريخ">Date</th>
|
||||
<th data-en="Valid Until" data-ar="صالح حتى">Valid Until</th>
|
||||
<th data-en="Customer" data-ar="العميل">Customer</th>
|
||||
<th data-en="Status" data-ar="الحالة">Status</th>
|
||||
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
|
||||
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
foreach ($data['quotations'] as $q):
|
||||
$items = db()->prepare("SELECT qi.*, i.name_en, i.name_ar, i.vat_rate
|
||||
FROM quotation_items qi
|
||||
JOIN stock_items i ON qi.item_id = i.id
|
||||
WHERE qi.quotation_id = ?");
|
||||
$items->execute([$q['id']]);
|
||||
$q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<tr>
|
||||
<td>QUO-<?= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
||||
<td><?= $q['quotation_date'] ?></td>
|
||||
<td><?= $q['valid_until'] ?: '---' ?></td>
|
||||
<td><?= htmlspecialchars($q['customer_name'] ?? '---') ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$statusClass = 'bg-secondary';
|
||||
if ($q['status'] === 'converted') $statusClass = 'bg-success';
|
||||
elseif ($q['status'] === 'pending') $statusClass = 'bg-warning text-dark';
|
||||
elseif ($q['status'] === 'expired' || $q['status'] === 'cancelled') $statusClass = 'bg-danger';
|
||||
?>
|
||||
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars($q['status']) ?></span>
|
||||
</td>
|
||||
<td class="text-end fw-bold">OMR <?= number_format((float)$q['total_with_vat'], 3) ?></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-info view-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-outline-secondary" onclick="window.viewAndPrintQuotation(<?= htmlspecialchars(json_encode($q)) ?>)" title="Print"><i class="bi bi-printer"></i></button>
|
||||
<button class="btn btn-outline-primary edit-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editQuotationModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
||||
<?php if ($q['status'] === 'pending'): ?>
|
||||
<button class="btn btn-outline-success convert-quotation-btn" data-id="<?= $q['id'] ?>" title="Convert to Invoice"><i class="bi bi-receipt"></i></button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-outline-danger" onclick="if(confirm('Delete this quotation?')) { const f = document.createElement('form'); f.method='POST'; f.innerHTML='<input type=hidden name=delete_quotation><input type=hidden name=id value=<?= $q['id'] ?>>'; document.body.appendChild(f); f.submit(); }" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($data['quotations'])): ?>
|
||||
<tr><td colspan="7" class="text-center py-4 text-muted" data-en="No quotations found" data-ar="لا توجد عروض أسعار">No quotations found</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($page === 'sales' || $page === 'purchases'): ?>
|
||||
<div class="card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@ -3470,10 +3751,175 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
const invoiceType = '<?= $page === "sales" ? "sale" : ($page === "purchases" ? "purchase" : "") ?>';
|
||||
const invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : ($page === "purchases" ? "purchase" : "") ?>';
|
||||
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
||||
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|
||||
|
||||
// Quotation Form Logic
|
||||
initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
||||
initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
||||
|
||||
document.querySelectorAll('.edit-quotation-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const data = JSON.parse(this.dataset.json);
|
||||
document.getElementById('edit_quotation_id').value = data.id;
|
||||
document.getElementById('edit_quot_customer_id').value = data.customer_id;
|
||||
document.getElementById('edit_quot_date').value = data.quotation_date;
|
||||
document.getElementById('edit_quot_valid').value = data.valid_until || '';
|
||||
document.getElementById('edit_quot_status').value = data.status || 'pending';
|
||||
|
||||
const tableBody = document.getElementById('editQuotItemsTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
data.items.forEach(item => {
|
||||
addItemToTable({
|
||||
id: item.item_id,
|
||||
name_en: item.name_en,
|
||||
name_ar: item.name_ar,
|
||||
sku: '',
|
||||
vat_rate: item.vat_rate || 0
|
||||
}, tableBody, null, null,
|
||||
document.getElementById('edit_quot_grand_display'),
|
||||
document.getElementById('edit_quot_subtotal_display'),
|
||||
document.getElementById('edit_quot_vat_display'),
|
||||
{ quantity: item.quantity, unit_price: item.unit_price });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.convert-quotation-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
if (confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
||||
const f = document.createElement('form');
|
||||
f.method = 'POST';
|
||||
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${this.dataset.id}">`;
|
||||
document.body.appendChild(f);
|
||||
f.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// View Quotation Logic
|
||||
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal'));
|
||||
const content = document.getElementById('quotationPrintableArea');
|
||||
|
||||
let itemsHtml = '';
|
||||
data.items.forEach((item, index) => {
|
||||
itemsHtml += `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
||||
<td class="text-center">${item.quantity}</td>
|
||||
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||
<td class="text-center">${item.vat_rate}%</td>
|
||||
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="p-5">
|
||||
<div class="d-flex justify-content-between mb-4 border-bottom pb-3">
|
||||
<div>
|
||||
<h3 class="text-primary fw-bold">QUOTATION</h3>
|
||||
<p class="mb-0"><strong>No:</strong> QUO-${data.id.toString().padStart(5, '0')}</p>
|
||||
<p class="mb-0"><strong>Date:</strong> ${data.quotation_date}</p>
|
||||
<p class="mb-0"><strong>Valid Until:</strong> ${data.valid_until || 'N/A'}</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<h4 class="fw-bold">${data.customer_name || 'Walk-in Customer'}</h4>
|
||||
<p class="mb-0">Status: <span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-secondary'}">${data.status.toUpperCase()}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="bg-dark text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Item Description</th>
|
||||
<th class="text-center">Qty</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-center">VAT</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${itemsHtml}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="5" class="text-end">Subtotal</th>
|
||||
<td class="text-end fw-bold">${parseFloat(data.total_amount).toFixed(3)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="5" class="text-end">VAT Amount</th>
|
||||
<td class="text-end fw-bold">${parseFloat(data.vat_amount).toFixed(3)}</td>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<th colspan="5" class="text-end h5">Grand Total (OMR)</th>
|
||||
<td class="text-end h5 fw-bold">${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="mt-5 pt-3 border-top">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<p class="small text-muted">Terms & Conditions:</p>
|
||||
<ul class="small text-muted">
|
||||
<li>Quotation is valid until the date mentioned above.</li>
|
||||
<li>Prices are inclusive of VAT where applicable.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6 text-end pt-4">
|
||||
<div class="border-top d-inline-block px-5">Authorized Signature</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const actionButtons = document.getElementById('quotationActionButtons');
|
||||
actionButtons.innerHTML = '';
|
||||
if (data.status === 'pending') {
|
||||
const convertBtn = document.createElement('button');
|
||||
convertBtn.className = 'btn btn-success me-2';
|
||||
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> Convert to Invoice';
|
||||
convertBtn.onclick = function() {
|
||||
if (confirm('Convert this quotation to an invoice?')) {
|
||||
const f = document.createElement('form');
|
||||
f.method = 'POST';
|
||||
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
||||
document.body.appendChild(f);
|
||||
f.submit();
|
||||
}
|
||||
};
|
||||
actionButtons.appendChild(convertBtn);
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'btn btn-primary';
|
||||
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> Edit';
|
||||
editBtn.onclick = function() {
|
||||
const editModal = new bootstrap.Modal(document.getElementById('editQuotationModal'));
|
||||
modal.hide();
|
||||
// Trigger the existing edit button click logic or manually populate
|
||||
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) ||
|
||||
document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`);
|
||||
if (originalEditBtn) originalEditBtn.click();
|
||||
};
|
||||
actionButtons.appendChild(editBtn);
|
||||
}
|
||||
|
||||
modal.show();
|
||||
if (autoPrint) {
|
||||
setTimeout(() => { window.print(); }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll('.view-quotation-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const data = JSON.parse(this.dataset.json);
|
||||
window.viewAndPrintQuotation(data, false);
|
||||
});
|
||||
});
|
||||
|
||||
// Edit Invoice Logic
|
||||
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
@ -3747,6 +4193,194 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Quotation Modal -->
|
||||
<div class="modal fade" id="addQuotationModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" data-en="Create New Quotation" data-ar="إنشاء عرض سعر جديد">Create New Quotation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
||||
<select name="customer_id" class="form-select" required>
|
||||
<option value="">---</option>
|
||||
<?php foreach ($data['customers_list'] as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
||||
<input type="date" name="quotation_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold" data-en="Valid Until" data-ar="صالح حتى">Valid Until</label>
|
||||
<input type="date" name="valid_until" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body bg-light">
|
||||
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" id="quotProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
||||
<div id="quotSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
||||
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
||||
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
||||
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
||||
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
||||
<th style="width: 5%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="quotItemsTableBody"></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
||||
<td class="text-end fw-bold" id="quot_subtotal_display">OMR 0.000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
||||
<td class="text-end fw-bold" id="quot_vat_display">OMR 0.000</td>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="quot_grand_display">OMR 0.000</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="add_quotation" class="btn btn-primary" data-en="Create Quotation" data-ar="إنشاء عرض السعر">Create Quotation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Quotation Modal -->
|
||||
<div class="modal fade" id="editQuotationModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" data-en="Edit Quotation" data-ar="تعديل عرض السعر">Edit Quotation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="quotation_id" id="edit_quotation_id">
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
||||
<select name="customer_id" id="edit_quot_customer_id" class="form-select" required>
|
||||
<option value="">---</option>
|
||||
<?php foreach ($data['customers_list'] as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
||||
<input type="date" name="quotation_date" id="edit_quot_date" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold" data-en="Valid Until" data-ar="صالح حتى">Valid Until</label>
|
||||
<input type="date" name="valid_until" id="edit_quot_valid" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="status" id="edit_quot_status" class="form-select">
|
||||
<option value="pending">Pending</option>
|
||||
<option value="converted">Converted</option>
|
||||
<option value="expired">Expired</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body bg-light">
|
||||
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" id="editQuotProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
||||
<div id="editQuotSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
||||
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
||||
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
||||
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
||||
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
||||
<th style="width: 5%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="editQuotItemsTableBody"></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
||||
<td class="text-end fw-bold" id="edit_quot_subtotal_display">OMR 0.000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
||||
<td class="text-end fw-bold" id="edit_quot_vat_display">OMR 0.000</td>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="edit_quot_grand_display">OMR 0.000</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="edit_quotation" class="btn btn-primary" data-en="Update Quotation" data-ar="تحديث عرض السعر">Update Quotation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Quotation Modal -->
|
||||
<div class="modal fade" id="viewQuotationModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-info text-white d-print-none">
|
||||
<h5 class="modal-title" data-en="View Quotation" data-ar="عرض سعر">View Quotation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0" id="quotationPrintableArea">
|
||||
<!-- Dynamic content -->
|
||||
</div>
|
||||
<div class="modal-footer d-print-none">
|
||||
<div id="quotationActionButtons" class="me-auto"></div>
|
||||
<button type="button" class="btn btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i> Print</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.no-print, .sidebar, .topbar, .card, .btn, .modal-header, .modal-footer, .d-print-none, .table-responsive,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user