From ea63a5146beecc05e3547448719833e828417047 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 16:12:25 +0000 Subject: [PATCH] add expired items --- assets/css/custom.css | 4 + assets/js/main.js | 25 +- db/migrations/20260217_sales_returns.sql | 23 + index.php | 725 +++++++++++++++++++++-- 4 files changed, 729 insertions(+), 48 deletions(-) create mode 100644 db/migrations/20260217_sales_returns.sql diff --git a/assets/css/custom.css b/assets/css/custom.css index a503557..bb6f174 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -235,6 +235,10 @@ body { border-bottom: 2px solid var(--border); } +.op-50 { + opacity: 0.5; +} + /* RTL Adjustments */ [dir="rtl"] .ms-auto { margin-right: auto !important; diff --git a/assets/js/main.js b/assets/js/main.js index d809544..15b0542 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -20,7 +20,30 @@ function setLanguage(lang) { // Update UI text document.querySelectorAll('[data-en]').forEach(el => { - el.textContent = el.getAttribute(`data-${lang}`); + const text = el.getAttribute(`data-${lang}`); + if (text) { + // If it's a simple element with only text, update it + // If it has children, we should only update the text node parts + // But for simplicity in this app, we'll check if it has any children + if (el.children.length === 0) { + el.textContent = text; + } else { + // If it has children (like icons), we try to find a text node to update + // or just update the first text node child + let found = false; + for (let node of el.childNodes) { + if (node.nodeType === 3) { // Text node + node.textContent = text; + found = true; + break; + } + } + if (!found) { + // Fallback: append text if no text node exists + el.appendChild(document.createTextNode(text)); + } + } + } }); // Update buttons/inputs diff --git a/db/migrations/20260217_sales_returns.sql b/db/migrations/20260217_sales_returns.sql new file mode 100644 index 0000000..c8a8564 --- /dev/null +++ b/db/migrations/20260217_sales_returns.sql @@ -0,0 +1,23 @@ +-- Migration: Add Sales Returns tables +CREATE TABLE IF NOT EXISTS sales_returns ( + id INT AUTO_INCREMENT PRIMARY KEY, + invoice_id INT NOT NULL, + customer_id INT NULL, + return_date DATE NOT NULL, + total_amount DECIMAL(15, 3) NOT NULL DEFAULT 0, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE, + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS sales_return_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + return_id INT NOT NULL, + item_id INT NOT NULL, + quantity DECIMAL(15, 2) NOT NULL, + unit_price DECIMAL(15, 3) NOT NULL, + total_price DECIMAL(15, 3) NOT NULL, + FOREIGN KEY (return_id) REFERENCES sales_returns(id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES stock_items(id) ON DELETE CASCADE +); diff --git a/index.php b/index.php index 1195feb..7e49dab 100644 --- a/index.php +++ b/index.php @@ -104,6 +104,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) { echo json_encode(['success' => true, 'points' => (float)$points]); exit; } + if ($_GET['action'] === 'get_invoice_items') { + header('Content-Type: application/json'); + $invoice_id = (int)$_GET['invoice_id']; + $stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku + FROM invoice_items ii + JOIN stock_items i ON ii.item_id = i.id + WHERE ii.invoice_id = ?"); + $stmt->execute([$invoice_id]); + echo json_encode($stmt->fetchAll()); + exit; + } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -1046,6 +1057,72 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } } + + if (isset($_POST['add_sales_return'])) { + $invoice_id = (int)$_POST['invoice_id']; + $return_date = $_POST['return_date'] ?: date('Y-m-d'); + $notes = $_POST['notes'] ?? ''; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($invoice_id && !empty($item_ids)) { + $db = db(); + $db->beginTransaction(); + try { + // Get customer ID from invoice + $stmt = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?"); + $stmt->execute([$invoice_id]); + $customer_id = $stmt->fetchColumn(); + + $total_return_amount = 0; + $items_data = []; + foreach ($item_ids as $index => $item_id) { + $qty = (float)$quantities[$index]; + $price = (float)$prices[$index]; + if ($qty <= 0) continue; + + $line_total = $qty * $price; + $total_return_amount += $line_total; + + $items_data[] = [ + 'id' => $item_id, + 'qty' => $qty, + 'price' => $price, + 'total' => $line_total + ]; + } + + if (empty($items_data)) throw new Exception("No items to return"); + + // Create Sales Return record + $stmt = $db->prepare("INSERT INTO sales_returns (invoice_id, customer_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $customer_id, $return_date, $total_return_amount, $notes]); + $return_id = $db->lastInsertId(); + + foreach ($items_data as $item) { + $stmt = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$return_id, $item['id'], $item['qty'], $item['price'], $item['total']]); + + // Restore stock + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); + $stmt->execute([$item['qty'], $item['id']]); + } + + // If it was a credit sale, we might want to reduce the customer balance. + if ($customer_id) { + $stmt = $db->prepare("UPDATE customers SET balance = balance + ? WHERE id = ?"); + $stmt->execute([$total_return_amount, $customer_id]); + } + + $db->commit(); + $message = "Sales Return #$return_id processed successfully!"; + } catch (Exception $e) { + if (isset($db)) $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } } @@ -1252,6 +1329,27 @@ switch ($page) { $data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll(); break; + case 'sales_returns': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['search'])) { + $where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ?)"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT sr.*, c.name as customer_name, i.total_with_vat as invoice_total + FROM sales_returns sr + LEFT JOIN customers c ON sr.customer_id = c.id + LEFT JOIN invoices i ON sr.invoice_id = i.id + WHERE $whereSql + ORDER BY sr.id DESC"); + $stmt->execute($params); + $data['returns'] = $stmt->fetchAll(); + $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'sale' ORDER BY id DESC")->fetchAll(); + break; + case 'customer_statement': case 'supplier_statement': $type = ($page === 'customer_statement') ? 'customer' : 'supplier'; @@ -1335,6 +1433,25 @@ switch ($page) { $stmt->execute([$start_date, $end_date]); $data['total_expenses'] = $stmt->fetchColumn() ?: 0; break; + case 'expiry_report': + $where = ["expiry_date IS NOT NULL"]; + $params = []; + $filter = $_GET['filter'] ?? 'all'; + if ($filter === 'expired') { + $where[] = "expiry_date <= CURDATE()"; + } elseif ($filter === 'near_expiry') { + $where[] = "expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)"; + } + + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar + FROM stock_items i + LEFT JOIN stock_categories c ON i.category_id = c.id + WHERE $whereSql + ORDER BY i.expiry_date ASC"); + $stmt->execute($params); + $data['expiry_items'] = $stmt->fetchAll(); + break; default: $data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll(); // Dashboard stats @@ -1345,9 +1462,15 @@ switch ($page) { 'total_received' => db()->query("SELECT SUM(amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.type = 'sale'")->fetchColumn() ?: 0, 'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM invoices WHERE type = 'purchase'")->fetchColumn() ?: 0, 'total_paid' => db()->query("SELECT SUM(amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.type = 'purchase'")->fetchColumn() ?: 0, + 'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(), + 'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(), ]; $data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received']; $data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid']; + + // Sales Chart Data + $data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices WHERE type = 'sale' GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); + $data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices WHERE type = 'sale' GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); break; } @@ -1368,9 +1491,11 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + +