adding purchase due invoices alert

This commit is contained in:
Flatlogic Bot 2026-02-20 03:25:34 +00:00
parent a832f10e69
commit a9484484d5
3 changed files with 103 additions and 13 deletions

View File

@ -0,0 +1,4 @@
-- Migration: Add due_date to invoices
-- Created at: 2026-02-20
ALTER TABLE invoices ADD COLUMN due_date DATE DEFAULT NULL;

109
index.php
View File

@ -128,6 +128,20 @@ function can(string $permission): bool {
return is_array($perms) && in_array($permission, $perms);
}
function getPurchaseAlerts() {
if (($_SESSION['user_role_name'] ?? '') !== 'Administrator' && ($_SESSION['user_permissions'] ?? '') !== 'all') return [];
$db = db();
$stmt = $db->query("SELECT i.id, i.due_date, i.total_with_vat, c.name as supplier_name
FROM invoices i
LEFT JOIN customers c ON i.customer_id = c.id
WHERE i.type = 'purchase'
AND i.status != 'paid'
AND i.due_date IS NOT NULL
AND i.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)
ORDER BY i.due_date ASC");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Missing helper functions
function getLoyaltyMultiplier($tier) {
switch (strtolower((string)$tier)) {
@ -639,6 +653,7 @@ function getPromotionalPrice($item) {
$type = $_POST['type'] ?? 'sale';
$cust_id = (int)$_POST['customer_id'];
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
@ -669,8 +684,8 @@ function getPromotionalPrice($item) {
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$stmt = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $type, $inv_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
$stmt = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $type, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
@ -922,6 +937,7 @@ function getPromotionalPrice($item) {
$id = (int)$_POST['invoice_id'];
$cust_id = (int)$_POST['customer_id'];
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
@ -951,8 +967,8 @@ function getPromotionalPrice($item) {
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
$db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
// Revert stock for old items
$stmtOld = $db->prepare("SELECT ii.item_id, ii.quantity, i.type FROM invoice_items ii JOIN invoices i ON ii.invoice_id = i.id WHERE ii.invoice_id = ?");
@ -2733,6 +2749,40 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<h4 class="m-0"><?= $titles[$page][$lang] ?? $titles['dashboard'][$lang] ?></h4>
</div>
<div class="d-flex align-items-center">
<?php
$purchaseAlerts = getPurchaseAlerts();
if (!empty($purchaseAlerts)):
?>
<div class="dropdown me-3">
<button class="btn btn-outline-danger btn-sm position-relative" type="button" data-bs-toggle="dropdown">
<i class="fas fa-bell"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
<?= count($purchaseAlerts) ?>
</span>
</button>
<div class="dropdown-menu dropdown-menu-end shadow border-0 p-0" style="width: 300px;">
<div class="p-3 border-bottom bg-light">
<h6 class="m-0 fw-bold"><?= $lang === 'ar' ? 'تنبيهات المدفوعات' : 'Payment Alerts' ?></h6>
</div>
<div class="list-group list-group-flush" style="max-height: 300px; overflow-y: auto;">
<?php foreach ($purchaseAlerts as $alert):
$isOverdue = strtotime($alert['due_date']) < time();
?>
<a href="index.php?page=purchases&search=<?= $alert['id'] ?>" class="list-group-item list-group-item-action p-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge <?= $isOverdue ? 'bg-danger' : 'bg-warning text-dark' ?>">
<?= $isOverdue ? ($lang === 'ar' ? 'متأخر' : 'Overdue') : ($lang === 'ar' ? 'مستحق قريباً' : 'Due Soon') ?>
</span>
<small class="text-muted"><?= htmlspecialchars($alert['due_date']) ?></small>
</div>
<div class="fw-bold small"><?= htmlspecialchars($alert['supplier_name']) ?></div>
<div class="text-primary small">OMR <?= number_format($alert['total_with_vat'], 3) ?></div>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<div class="dropdown me-3">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-palette"></i> <span><?= $lang === 'ar' ? 'المظهر' : 'Theme' ?></span>
@ -2810,24 +2860,32 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?php if ($page === 'dashboard'): ?>
<?php if ($data['stats']['expired_items'] > 0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0): ?>
<?php
$purchaseAlertsCount = count(getPurchaseAlerts());
if ($data['stats']['expired_items'] > 0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0 || $purchaseAlertsCount > 0): ?>
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-warning border-0 shadow-sm d-flex align-items-center mb-0">
<i class="bi bi-exclamation-triangle-fill h4 mb-0 me-3 text-warning"></i>
<div class="flex-grow-1">
<span data-en="Inventory Alert:" data-ar="تنبيه المخزون:"><strong>Inventory Alert:</strong></span>
<span data-en="Administrative Alerts:" data-ar="تنبيهات إدارية:"><strong>Administrative Alerts:</strong></span>
<?php if ($data['stats']['expired_items'] > 0): ?>
<span data-en="<?= $data['stats']['expired_items'] ?> items have expired." data-ar="هنالك <?= $data['stats']['expired_items'] ?> صنف منتهي الصلاحية."><?= $data['stats']['expired_items'] ?> items have expired.</span>
<span class="ms-2" data-en="<?= $data['stats']['expired_items'] ?> items have expired." data-ar="هنالك <?= $data['stats']['expired_items'] ?> صنف منتهي الصلاحية."><?= $data['stats']['expired_items'] ?> items have expired.</span>
<?php endif; ?>
<?php if ($data['stats']['near_expiry_items'] > 0): ?>
<span data-en="<?= $data['stats']['near_expiry_items'] ?> items are expiring soon (within 30 days)." data-ar="هنالك <?= $data['stats']['near_expiry_items'] ?> صنف ستنتهي صلاحيتها قريباً (خلال 30 يوم)."><?= $data['stats']['near_expiry_items'] ?> items are expiring soon (within 30 days).</span>
<span class="ms-2" data-en="<?= $data['stats']['near_expiry_items'] ?> items are expiring soon." data-ar="هنالك <?= $data['stats']['near_expiry_items'] ?> صنف ستنتهي صلاحيتها قريباً."><?= $data['stats']['near_expiry_items'] ?> items are expiring soon.</span>
<?php endif; ?>
<?php if ($data['stats']['low_stock_items_count'] > 0): ?>
<span data-en="<?= $data['stats']['low_stock_items_count'] ?> items are below minimum level." data-ar="هنالك <?= $data['stats']['low_stock_items_count'] ?> صنف تحت الحد الأدنى للمخزون."><?= $data['stats']['low_stock_items_count'] ?> items are below minimum level.</span>
<span class="ms-2" data-en="<?= $data['stats']['low_stock_items_count'] ?> items are below minimum level." data-ar="هنالك <?= $data['stats']['low_stock_items_count'] ?> صنف تحت الحد الأدنى للمخزون."><?= $data['stats']['low_stock_items_count'] ?> items are below minimum level.</span>
<?php endif; ?>
<?php if ($purchaseAlertsCount > 0): ?>
<span class="ms-2 text-danger fw-bold" data-en="<?= $purchaseAlertsCount ?> purchase invoices are due or overdue." data-ar="هنالك <?= $purchaseAlertsCount ?> فاتورة مشتريات مستحقة أو متأخرة."><?= $purchaseAlertsCount ?> purchase invoices are due or overdue.</span>
<?php endif; ?>
</div>
<div class="d-flex gap-2">
<?php if ($purchaseAlertsCount > 0): ?>
<a href="index.php?page=purchases" class="btn btn-primary btn-sm" data-en="View Purchases" data-ar="عرض المشتريات">View Purchases</a>
<?php endif; ?>
<a href="index.php?page=expiry_report" class="btn btn-warning btn-sm" data-en="Expiry Report" data-ar="تقرير الانتهاء">Expiry Report</a>
<a href="index.php?page=low_stock_report" class="btn btn-danger btn-sm" data-en="Low Stock Report" data-ar="تقرير النواقص">Low Stock Report</a>
</div>
@ -4937,6 +4995,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<tr>
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
<th data-en="Date" data-ar="التاريخ">Date</th>
<th data-en="Due Date" data-ar="تاريخ الاستحقاق">Due Date</th>
<th data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></th>
<th data-en="Status" data-ar="الحالة">Status</th>
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
@ -4958,6 +5017,21 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<tr>
<td>INV-<?= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?></td>
<td><?= $inv['invoice_date'] ?></td>
<td>
<?php if ($inv['due_date']): ?>
<?php
$isOverdue = strtotime($inv['due_date']) < time() && $inv['status'] !== 'paid';
?>
<span class="<?= $isOverdue ? 'text-danger fw-bold' : '' ?>">
<?= $inv['due_date'] ?>
<?php if ($isOverdue): ?>
<i class="bi bi-exclamation-triangle-fill ms-1" title="Overdue"></i>
<?php endif; ?>
</span>
<?php else: ?>
---
<?php endif; ?>
</td>
<td><?= htmlspecialchars($inv['customer_name'] ?? '---') ?></td>
<td>
<?php
@ -9304,6 +9378,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('edit_invoice_id').value = data.id;
document.getElementById('edit_customer_id').value = data.customer_id;
document.getElementById('edit_invoice_date').value = data.invoice_date;
document.getElementById('edit_due_date').value = data.due_date || '';
document.getElementById('edit_payment_type').value = data.payment_type || 'cash';
document.getElementById('edit_status').value = data.status || 'unpaid';
document.getElementById('edit_paid_amount').value = parseFloat(data.paid_amount || 0).toFixed(3);
@ -9936,7 +10011,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="hidden" name="type" value="<?= $page === 'sales' ? 'sale' : 'purchase' ?>">
<div class="modal-body p-4">
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
<select name="customer_id" class="form-select select2" required>
<option value="">---</option>
@ -9949,6 +10024,10 @@ document.addEventListener('DOMContentLoaded', function() {
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
<input type="date" name="invoice_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-md-2">
<label class="form-label fw-bold" data-en="Due Date" data-ar="تاريخ الاستحقاق">Due Date</label>
<input type="date" name="due_date" class="form-control">
</div>
<div class="col-md-2">
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
<select name="payment_type" class="form-select">
@ -9958,7 +10037,7 @@ document.addEventListener('DOMContentLoaded', function() {
<option value="credit">Credit</option>
</select>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
<select name="status" id="add_status" class="form-select">
<option value="unpaid">Unpaid</option>
@ -10036,7 +10115,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="hidden" name="invoice_id" id="edit_invoice_id">
<div class="modal-body p-4">
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
<select name="customer_id" id="edit_customer_id" class="form-select select2" required>
<option value="">---</option>
@ -10049,6 +10128,10 @@ document.addEventListener('DOMContentLoaded', function() {
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
<input type="date" name="invoice_date" id="edit_invoice_date" class="form-control" required>
</div>
<div class="col-md-2">
<label class="form-label fw-bold" data-en="Due Date" data-ar="تاريخ الاستحقاق">Due Date</label>
<input type="date" name="due_date" id="edit_due_date" class="form-control">
</div>
<div class="col-md-2">
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
<select name="payment_type" id="edit_payment_type" class="form-select">
@ -10058,7 +10141,7 @@ document.addEventListener('DOMContentLoaded', function() {
<option value="credit">Credit</option>
</select>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
<select name="status" id="edit_status" class="form-select">
<option value="unpaid">Unpaid</option>

View File

@ -111,3 +111,6 @@
2026-02-19 18:08:08 - POST: {"license_key":"FLAT-TEST-1234-5678","activate":""}
2026-02-19 18:15:44 - POST: {"license_key":"FLAT-5FDB-C2BB","activate":""}
2026-02-19 18:26:28 - POST: {"license_key":"FLAT-5FDB-C2BB","activate":""}
2026-02-20 03:06:13 - POST: {"id":"6","name":"Accountant","permissions":["accounting_view","accounting_add","accounting_edit","accounting_delete","trial_balance_view","trial_balance_add","trial_balance_edit","trial_balance_delete","profit_loss_view","profit_loss_add","profit_loss_edit","profit_loss_delete","balance_sheet_view","balance_sheet_add","balance_sheet_edit","balance_sheet_delete","vat_report_view","vat_report_add","vat_report_edit","vat_report_delete"],"edit_role_group":""}
2026-02-20 03:06:37 - POST: {"id":"1","name":"Administrator","permissions":["dashboard_view","dashboard_add","dashboard_edit","dashboard_delete","items_view","items_add","items_edit","items_delete","categories_view","categories_add","categories_edit","categories_delete","units_view","units_add","units_edit","units_delete","customers_view","customers_add","customers_edit","customers_delete","suppliers_view","suppliers_add","suppliers_edit","suppliers_delete","pos_view","pos_add","pos_edit","pos_delete","sales_view","sales_add","sales_edit","sales_delete","sales_returns_view","sales_returns_add","sales_returns_edit","sales_returns_delete","quotations_view","quotations_add","quotations_edit","quotations_delete","purchases_view","purchases_add","purchases_edit","purchases_delete","purchase_returns_view","purchase_returns_add","purchase_returns_edit","purchase_returns_delete","expense_categories_view","expense_categories_add","expense_categories_edit","expense_categories_delete","expenses_view","expenses_add","expenses_edit","expenses_delete","accounting_view","accounting_add","accounting_edit","accounting_delete","trial_balance_view","trial_balance_add","trial_balance_edit","trial_balance_delete","profit_loss_view","profit_loss_add","profit_loss_edit","profit_loss_delete","balance_sheet_view","balance_sheet_add","balance_sheet_edit","balance_sheet_delete","vat_report_view","vat_report_add","vat_report_edit","vat_report_delete","hr_departments_view","hr_departments_add","hr_departments_edit","hr_departments_delete","hr_employees_view","hr_employees_add","hr_employees_edit","hr_employees_delete","hr_attendance_view","hr_attendance_add","hr_attendance_edit","hr_attendance_delete","hr_payroll_view","hr_payroll_add","hr_payroll_edit","hr_payroll_delete","customer_statement_view","customer_statement_add","customer_statement_edit","customer_statement_delete","supplier_statement_view","supplier_statement_add","supplier_statement_edit","supplier_statement_delete","cashflow_report_view","cashflow_report_add","cashflow_report_edit","cashflow_report_delete","expiry_report_view","expiry_report_add","expiry_report_edit","expiry_report_delete","low_stock_report_view","low_stock_report_add","low_stock_report_edit","low_stock_report_delete","loyalty_history_view","loyalty_history_add","loyalty_history_edit","loyalty_history_delete","payment_methods_view","payment_methods_add","payment_methods_edit","payment_methods_delete","devices_view","devices_add","devices_edit","devices_delete","settings_view","settings_add","settings_edit","settings_delete","role_groups_view","role_groups_add","role_groups_edit","role_groups_delete","users_view","users_add","users_edit","users_delete","cash_registers_view","cash_registers_add","cash_registers_edit","cash_registers_delete","register_sessions_view","register_sessions_add","register_sessions_edit","register_sessions_delete","scale_devices_view","scale_devices_add","scale_devices_edit","scale_devices_delete","customer_display_settings_view","customer_display_settings_add","customer_display_settings_edit","customer_display_settings_delete","backups_view","backups_add","backups_edit","backups_delete","logs_view","logs_add","logs_edit","logs_delete"],"edit_role_group":""}
2026-02-20 03:07:31 - POST: {"id":"4","name":"Cashier","permissions":["items_view","items_add","pos_view","pos_add","sales_view","sales_add"],"edit_role_group":""}