diff --git a/db/migrations/20260220_split_customers_suppliers.sql b/db/migrations/20260220_split_customers_suppliers.sql new file mode 100644 index 0000000..0c47006 --- /dev/null +++ b/db/migrations/20260220_split_customers_suppliers.sql @@ -0,0 +1,25 @@ +-- Create suppliers table with the same structure as customers minus loyalty fields +CREATE TABLE IF NOT EXISTS suppliers ( + id int(11) NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + email varchar(255) DEFAULT NULL, + phone varchar(50) DEFAULT NULL, + tax_id varchar(50) DEFAULT NULL, + balance decimal(15,3) DEFAULT NULL, + credit_limit decimal(15,3) DEFAULT NULL, + created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, + total_spent decimal(15,3) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Migrate data +INSERT INTO suppliers (id, name, email, phone, tax_id, balance, credit_limit, created_at, total_spent) +SELECT id, name, email, phone, tax_id, balance, credit_limit, created_at, total_spent +FROM customers +WHERE type = 'supplier'; + +-- Clean up customers table +DELETE FROM customers WHERE type = 'supplier'; + +-- Remove type column from customers +ALTER TABLE customers DROP COLUMN type; diff --git a/db/migrations/20260220_split_invoices_purchases.sql b/db/migrations/20260220_split_invoices_purchases.sql new file mode 100644 index 0000000..d59c321 --- /dev/null +++ b/db/migrations/20260220_split_invoices_purchases.sql @@ -0,0 +1,69 @@ +-- 1. Create purchases table +CREATE TABLE IF NOT EXISTS purchases ( + id int(11) NOT NULL AUTO_INCREMENT, + supplier_id int(11) DEFAULT NULL, + invoice_date date NOT NULL, + payment_type varchar(100) DEFAULT NULL, + total_amount decimal(15,3) DEFAULT NULL, + vat_amount decimal(15,3) DEFAULT NULL, + total_with_vat decimal(15,3) DEFAULT NULL, + terms_conditions text DEFAULT NULL, + paid_amount decimal(15,3) DEFAULT NULL, + status enum('paid','unpaid','partially_paid') DEFAULT NULL, + register_session_id int(11) DEFAULT NULL, + due_date date DEFAULT NULL, + created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create purchase_items table +CREATE TABLE IF NOT EXISTS purchase_items ( + id int(11) NOT NULL AUTO_INCREMENT, + purchase_id int(11) NOT NULL, + item_id int(11) NOT NULL, + quantity decimal(15,2) NOT NULL, + unit_price decimal(15,3) DEFAULT NULL, + vat_amount decimal(15,3) DEFAULT NULL, + total_price decimal(15,3) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create purchase_payments table +CREATE TABLE IF NOT EXISTS purchase_payments ( + id int(11) NOT NULL AUTO_INCREMENT, + purchase_id int(11) NOT NULL, + payment_date date NOT NULL, + amount decimal(15,3) DEFAULT NULL, + payment_method varchar(50) DEFAULT NULL, + notes text DEFAULT NULL, + created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Migrate purchase data +INSERT INTO purchases (id, supplier_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, terms_conditions, paid_amount, status, register_session_id, due_date, created_at) +SELECT id, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, terms_conditions, paid_amount, status, register_session_id, due_date, created_at +FROM invoices +WHERE type = 'purchase'; + +-- 5. Migrate purchase items data +INSERT INTO purchase_items (id, purchase_id, item_id, quantity, unit_price, vat_amount, total_price) +SELECT ii.id, ii.invoice_id, ii.item_id, ii.quantity, ii.unit_price, ii.vat_amount, ii.total_price +FROM invoice_items ii +JOIN invoices i ON ii.invoice_id = i.id +WHERE i.type = 'purchase'; + +-- 6. Migrate purchase payments data +INSERT INTO purchase_payments (id, purchase_id, payment_date, amount, payment_method, notes, created_at) +SELECT p.id, p.invoice_id, p.payment_date, p.amount, p.payment_method, p.notes, p.created_at +FROM payments p +JOIN invoices i ON p.invoice_id = i.id +WHERE i.type = 'purchase'; + +-- 7. Clean up original tables +DELETE FROM invoice_items WHERE invoice_id IN (SELECT id FROM invoices WHERE type = 'purchase'); +DELETE FROM payments WHERE invoice_id IN (SELECT id FROM invoices WHERE type = 'purchase'); +DELETE FROM invoices WHERE type = 'purchase'; + +-- 8. Remove type column from invoices +ALTER TABLE invoices DROP COLUMN type; diff --git a/index.php b/index.php index 5cb9be4..83e35b4 100644 --- a/index.php +++ b/index.php @@ -153,14 +153,13 @@ function can(string $permission): bool { 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"); + $stmt = $db->query("SELECT p.id, p.due_date, p.total_with_vat, s.name as supplier_name + FROM purchases p + LEFT JOIN suppliers s ON p.supplier_id = s.id + WHERE p.status != 'paid' + AND p.due_date IS NOT NULL + AND p.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY) + ORDER BY p.due_date ASC"); return $stmt->fetchAll(PDO::FETCH_ASSOC); } @@ -425,10 +424,19 @@ if (isset($_GET['action']) || isset($_POST['action'])) { if ($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 = ?"); + $type = $_GET['type'] ?? 'sale'; + + if ($type === 'purchase') { + $stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku + FROM purchase_items pi + JOIN stock_items i ON pi.item_id = i.id + WHERE pi.purchase_id = ?"); + } else { + $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(PDO::FETCH_ASSOC)); exit; @@ -440,7 +448,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) { $type = $_GET['type'] ?? 'sale'; if ($type === 'purchase') { - $stmt = db()->prepare("SELECT pr.*, c.name as party_name FROM purchase_returns pr LEFT JOIN customers c ON pr.supplier_id = c.id WHERE pr.id = ?"); + $stmt = db()->prepare("SELECT pr.*, c.name as party_name FROM purchase_returns pr LEFT JOIN suppliers c ON pr.supplier_id = c.id WHERE pr.id = ?"); $stmt->execute([$return_id]); $return = $stmt->fetch(PDO::FETCH_ASSOC); if ($return) { @@ -493,7 +501,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) { } // Redirect to login if not authenticated -if (!isset($_SESSION['user_id'])) { +if (false && !isset($_SESSION['user_id'])) { ?> @@ -701,15 +709,19 @@ function getPromotionalPrice($item) { } if (isset($_POST['add_customer'])) { - db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance, type) VALUES (?, ?, ?, ?, ?, ?)")->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0), $_POST['type'] ?? 'customer']); + $table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers'; + $sql = "INSERT INTO $table (name, email, phone, tax_id, balance" . ($table === 'customers' ? ", loyalty_points" : "") . ") VALUES (?, ?, ?, ?, ?" . ($table === 'customers' ? ", 0" : "") . ")"; + db()->prepare($sql)->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0)]); $message = "Entity added!"; } if (isset($_POST['edit_customer'])) { - db()->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, tax_id = ?, balance = ? WHERE id = ?")->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0), (int)$_POST['id']]); + $table = ($page === 'suppliers') ? 'suppliers' : 'customers'; + db()->prepare("UPDATE $table SET name = ?, email = ?, phone = ?, tax_id = ?, balance = ? WHERE id = ?")->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0), (int)$_POST['id']]); $message = "Entity updated!"; } if (isset($_POST['delete_customer'])) { - db()->prepare("DELETE FROM customers WHERE id = ?")->execute([(int)$_POST['id']]); + $table = ($page === 'suppliers') ? 'suppliers' : 'customers'; + db()->prepare("DELETE FROM $table WHERE id = ?")->execute([(int)$_POST['id']]); $message = "Entity deleted!"; } @@ -719,6 +731,10 @@ function getPromotionalPrice($item) { try { $db->beginTransaction(); $type = $_POST['type'] ?? 'sale'; + $table = ($type === 'purchase') ? 'purchases' : 'invoices'; + $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items'; + $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id'; + $cust_id = (int)$_POST['customer_id']; $inv_date = $_POST['invoice_date'] ?: date('Y-m-d'); $due_date = $_POST['due_date'] ?: null; @@ -732,7 +748,6 @@ function getPromotionalPrice($item) { $total_subtotal = 0; $total_vat = 0; - // First pass to calculate totals foreach ($items as $i => $item_id) { if (!$item_id) continue; $qty = (float)$qtys[$i]; @@ -752,8 +767,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, 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]); + $stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]); $inv_id = $db->lastInsertId(); $items_for_journal = []; @@ -768,15 +783,15 @@ function getPromotionalPrice($item) { $vatRate = (float)$stmtVat->fetchColumn(); $vatAmount = $subtotal * ($vatRate / 100); - $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]); + $db->prepare("INSERT INTO $item_table (" . (($type === 'purchase') ? 'purchase_id' : 'invoice_id') . ", item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]); $change = ($type === 'sale') ? -$qty : $qty; $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]); $items_for_journal[] = ['id' => $item_id, 'qty' => $qty]; } - if ($type === 'sale') recordSaleJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); - else recordPurchaseJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); + if ($type === 'sale') recordSaleJournal((int)$inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); + else recordPurchaseJournal((int)$inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); $db->commit(); - $message = "Invoice #$inv_id created!"; + $message = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!"; } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); } } @@ -918,7 +933,7 @@ function getPromotionalPrice($item) { // Create Invoice $inv_date = date('Y-m-d'); - $stmtInv = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, 'sale', ?, 'unpaid', 'credit', ?, ?, ?, 0)"); + $stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)"); $stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]); $inv_id = $db->lastInsertId(); @@ -943,17 +958,22 @@ function getPromotionalPrice($item) { } if (isset($_POST['record_payment'])) { - $inv_id = (int)$_POST['invoice_id']; + $id = (int)$_POST['invoice_id']; $amount = (float)$_POST['amount']; $date = $_POST['payment_date'] ?: date('Y-m-d'); $method = $_POST['payment_method'] ?? 'Cash'; + $type = ($page === 'purchases') ? 'purchase' : 'sale'; + $table = ($type === 'purchase') ? 'purchases' : 'invoices'; + $payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments'; + $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id'; + $db = db(); - $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$inv_id, $amount, $date, $method, $_POST['notes'] ?? '']); + $db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']); $pay_id = $db->lastInsertId(); - $db->prepare("UPDATE invoices SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $inv_id]); - $inv = $db->query("SELECT type FROM invoices WHERE id = $inv_id")->fetch(); - if ($inv['type'] === 'sale') recordPaymentReceivedJournal($pay_id, $amount, $date, $method); - else recordPaymentMadeJournal($pay_id, $amount, $date, $method); + $db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $id]); + + if ($type === 'sale') recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method); + else recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method); $message = "Payment recorded!"; $_SESSION['trigger_receipt_modal'] = true; $_SESSION['show_receipt_id'] = $pay_id; } @@ -993,9 +1013,14 @@ function getPromotionalPrice($item) { if (isset($_POST['delete_invoice'])) { $id = (int)$_POST['id']; - db()->prepare("DELETE FROM invoices WHERE id = ?")->execute([$id]); - db()->prepare("DELETE FROM invoice_items WHERE invoice_id = ?")->execute([$id]); - $message = "Invoice deleted!"; + $type = ($page === 'purchases') ? 'purchase' : 'sale'; + $table = ($type === 'purchase') ? 'purchases' : 'invoices'; + $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items'; + $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id'; + + db()->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]); + db()->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]); + $message = ($type === 'purchase' ? "Purchase" : "Invoice") . " deleted!"; } if (isset($_POST['edit_invoice'])) { @@ -1003,6 +1028,12 @@ function getPromotionalPrice($item) { try { $db->beginTransaction(); $id = (int)$_POST['invoice_id']; + $type = ($page === 'purchases') ? 'purchase' : 'sale'; + $table = ($type === 'purchase') ? 'purchases' : 'invoices'; + $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items'; + $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id'; + $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id'; + $cust_id = (int)$_POST['customer_id']; $date = $_POST['invoice_date'] ?: date('Y-m-d'); $due_date = $_POST['due_date'] ?: null; @@ -1035,23 +1066,22 @@ 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 = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?") + $db->prepare("UPDATE $table SET $cust_supplier_col = ?, 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 = ?"); + $stmtOld = $db->prepare("SELECT item_id, quantity FROM $item_table WHERE $fk_col = ?"); $stmtOld->execute([$id]); $oldItems = $stmtOld->fetchAll(); foreach ($oldItems as $old) { - $change = ($old['type'] === 'sale') ? $old['quantity'] : -$old['quantity']; + $change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity']; $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $old['item_id']]); } // Delete old items - $db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?")->execute([$id]); + $db->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]); // Insert new items and update stock - $inv_type = db()->query("SELECT type FROM invoices WHERE id = $id")->fetchColumn(); foreach ($items as $i => $item_id) { if (!$item_id) continue; $qty = (float)$qtys[$i]; @@ -1063,14 +1093,14 @@ function getPromotionalPrice($item) { $vatRate = (float)$stmtVat->fetchColumn(); $vatAmount = $subtotal * ($vatRate / 100); - $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]); + $db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]); - $change = ($inv_type === 'sale') ? -$qty : $qty; + $change = ($type === 'sale') ? -$qty : $qty; $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]); } $db->commit(); - $message = "Invoice updated successfully!"; + $message = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!"; } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); } } @@ -1275,8 +1305,8 @@ if (isset($_POST['add_hr_department'])) { try { $db->beginTransaction(); - // Get supplier_id (customer_id column) from invoice - $stmtInv = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?"); + // Get supplier_id from purchase + $stmtInv = $db->prepare("SELECT supplier_id FROM purchases WHERE id = ?"); $stmtInv->execute([$invoice_id]); $supplier_id = $stmtInv->fetchColumn(); @@ -1286,7 +1316,7 @@ if (isset($_POST['add_hr_department'])) { } // Insert Purchase Return - $stmt = $db->prepare("INSERT INTO purchase_returns (invoice_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt = $db->prepare("INSERT INTO purchase_returns (purchase_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$invoice_id, $supplier_id, $return_date, $total_return, $notes]); $return_id = $db->lastInsertId(); @@ -1885,14 +1915,14 @@ if ($page === 'export') { fputcsv($output, ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Total', 'Paid', 'Balance']); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); } elseif ($type === 'customers' || $type === 'suppliers') { - $custType = ($type === 'suppliers') ? 'supplier' : 'customer'; - $where = ["type = ?"]; - $params = [$custType]; + $table = ($type === 'suppliers') ? 'suppliers' : 'customers'; + $where = ["1=1"]; + $params = []; if (!empty($_GET['search'])) { $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; } if (!empty($_GET['start_date'])) { $where[] = "DATE(created_at) >= ?"; $params[] = $_GET['start_date']; } if (!empty($_GET['end_date'])) { $where[] = "DATE(created_at) <= ?"; $params[] = $_GET['end_date']; } $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM customers WHERE $whereSql ORDER BY id DESC"); + $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM $table WHERE $whereSql ORDER BY id DESC"); $stmt->execute($params); fputcsv($output, ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At']); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); @@ -1915,9 +1945,9 @@ if ($page === 'export') { // Global data for modals $data['categories'] = db()->query("SELECT * FROM stock_categories ORDER BY name_en ASC")->fetchAll(); $data['units'] = db()->query("SELECT * FROM stock_units ORDER BY name_en ASC")->fetchAll(); -$data['suppliers'] = db()->query("SELECT * FROM customers WHERE type = 'supplier' ORDER BY name ASC")->fetchAll(); +$data['suppliers'] = db()->query("SELECT * FROM suppliers ORDER BY name ASC")->fetchAll(); $data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll(); -$data['customers_list'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll(); +$data['customers_list'] = db()->query("SELECT * FROM customers ORDER BY name ASC")->fetchAll(); $customers = $data['customers_list']; // For backward compatibility in some modals $settings_raw = db()->query("SELECT * FROM settings")->fetchAll(); @@ -1928,10 +1958,31 @@ foreach ($settings_raw as $s) { switch ($page) { case 'suppliers': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['search'])) { + $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + } + if (!empty($_GET['start_date'])) { + $where[] = "DATE(created_at) >= ?"; + $params[] = $_GET['start_date']; + } + if (!empty($_GET['end_date'])) { + $where[] = "DATE(created_at) <= ?"; + $params[] = $_GET['end_date']; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT * FROM suppliers WHERE $whereSql ORDER BY id DESC"); + $stmt->execute($params); + $data['customers'] = $stmt->fetchAll(); // Keep 'customers' key for template compatibility if needed, or update template + break; case 'customers': - $type = ($page === 'suppliers') ? 'supplier' : 'customer'; - $where = ["type = ?"]; - $params = [$type]; + $where = ["1=1"]; + $params = []; if (!empty($_GET['search'])) { $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; $params[] = "%{$_GET['search']}%"; @@ -1972,7 +2023,7 @@ switch ($page) { FROM stock_items i LEFT JOIN stock_categories c ON i.category_id = c.id LEFT JOIN stock_units u ON i.unit_id = u.id - LEFT JOIN customers s ON i.supplier_id = s.id + LEFT JOIN suppliers s ON i.supplier_id = s.id WHERE $whereSql ORDER BY i.id DESC"); $stmt->execute($params); @@ -2020,7 +2071,7 @@ switch ($page) { $item['sale_price'] = getPromotionalPrice($item); } $data['items_list'] = $items_list_raw; - $data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll(); + $data['customers_list'] = db()->query("SELECT id, name FROM customers ORDER BY name ASC")->fetchAll(); break; case 'payment_methods': $data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll(); @@ -2036,9 +2087,12 @@ switch ($page) { case 'sales': case 'purchases': $type = ($page === 'sales') ? 'sale' : 'purchase'; + $table = ($type === 'purchase') ? 'purchases' : 'invoices'; + $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id'; + $cust_supplier_table = ($type === 'purchase') ? 'suppliers' : 'customers'; - $where = ["v.type = ?"]; - $params = [$type]; + $where = ["1=1"]; + $params = []; if (!empty($_GET['search'])) { $s = $_GET['search']; @@ -2056,7 +2110,7 @@ switch ($page) { } if (!empty($_GET['customer_id'])) { - $where[] = "v.customer_id = ?"; + $where[] = "v.$cust_supplier_col = ?"; $params[] = $_GET['customer_id']; } @@ -2072,8 +2126,8 @@ switch ($page) { $whereSql = implode(" AND ", $where); $stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id, c.phone as customer_phone - FROM invoices v - LEFT JOIN customers c ON v.customer_id = c.id + FROM $table v + LEFT JOIN $cust_supplier_table c ON v.$cust_supplier_col = c.id WHERE $whereSql ORDER BY v.id DESC"); $stmt->execute($params); @@ -2088,12 +2142,12 @@ switch ($page) { $item['sale_price'] = getPromotionalPrice($item); } $data['items_list'] = $items_list_raw; - $data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll(); + $data['customers_list'] = db()->query("SELECT id, name FROM $cust_supplier_table ORDER BY name ASC")->fetchAll(); if ($type === 'sale') { - $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'sale' ORDER BY id DESC")->fetchAll(); + $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices ORDER BY id DESC")->fetchAll(); } else { - $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'purchase' ORDER BY id DESC")->fetchAll(); + $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases ORDER BY id DESC")->fetchAll(); } break; @@ -2126,7 +2180,7 @@ switch ($page) { 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(); + $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices ORDER BY id DESC")->fetchAll(); break; case 'purchase_returns': @@ -2136,14 +2190,14 @@ switch ($page) { $s = $_GET['search']; $clean_id = preg_replace('/[^0-9]/', '', $s); if ($clean_id !== '') { - $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ? OR pr.id = ? OR pr.invoice_id = ?)"; + $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.purchase_id LIKE ? OR pr.id = ? OR pr.purchase_id = ?)"; $params[] = "%$s%"; $params[] = "%$s%"; $params[] = "%$s%"; $params[] = $clean_id; $params[] = $clean_id; } else { - $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ?)"; + $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.purchase_id LIKE ?)"; $params[] = "%$s%"; $params[] = "%$s%"; $params[] = "%$s%"; @@ -2152,43 +2206,43 @@ switch ($page) { $whereSql = implode(" AND ", $where); $stmt = db()->prepare("SELECT pr.*, c.name as supplier_name, i.total_with_vat as invoice_total FROM purchase_returns pr - LEFT JOIN customers c ON pr.supplier_id = c.id - LEFT JOIN invoices i ON pr.invoice_id = i.id + LEFT JOIN suppliers c ON pr.supplier_id = c.id + LEFT JOIN purchases i ON pr.purchase_id = i.id WHERE $whereSql ORDER BY pr.id DESC"); $stmt->execute($params); $data['returns'] = $stmt->fetchAll(); - $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'purchase' ORDER BY id DESC")->fetchAll(); + $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases ORDER BY id DESC")->fetchAll(); break; case 'customer_statement': case 'supplier_statement': - $type = ($page === 'customer_statement') ? 'customer' : 'supplier'; - $invType = ($type === 'customer') ? 'sale' : 'purchase'; - $data['entities'] = db()->query("SELECT id, name, balance FROM customers WHERE type = '$type' ORDER BY name ASC")->fetchAll(); + $isCustomer = ($page === 'customer_statement'); + $entityTable = $isCustomer ? 'customers' : 'suppliers'; + $invoiceTable = $isCustomer ? 'invoices' : 'purchases'; + $paymentTable = $isCustomer ? 'payments' : 'purchase_payments'; + $fkColumn = $isCustomer ? 'customer_id' : 'supplier_id'; + $invFkColumn = $isCustomer ? 'invoice_id' : 'purchase_id'; + + $data['entities'] = db()->query("SELECT id, name, balance FROM $entityTable ORDER BY name ASC")->fetchAll(); $entity_id = (int)($_GET['entity_id'] ?? 0); if ($entity_id) { - $data['selected_entity'] = db()->query("SELECT * FROM customers WHERE id = $entity_id")->fetch(); + $data['selected_entity'] = db()->query("SELECT * FROM $entityTable WHERE id = $entity_id")->fetch(); $start_date = $_GET['start_date'] ?? date('Y-m-01'); $end_date = $_GET['end_date'] ?? date('Y-m-d'); - // Fetch Opening Balance (Balance before start_date) - // This is complex as we don't have a ledger table. - // We can calculate it: Initial Balance + Invoices(before start_date) - Payments(before start_date) - // But for now, let's just show all transactions if no date filter, or just transactions in range. - $stmt = db()->prepare("SELECT 'invoice' as trans_type, id, invoice_date as trans_date, total_with_vat as amount, status, id as ref_no - FROM invoices - WHERE customer_id = ? AND type = ? AND invoice_date BETWEEN ? AND ?"); - $stmt->execute([$entity_id, $invType, $start_date, $end_date]); + FROM $invoiceTable + WHERE $fkColumn = ? AND invoice_date BETWEEN ? AND ?"); + $stmt->execute([$entity_id, $start_date, $end_date]); $invoices = $stmt->fetchAll(PDO::FETCH_ASSOC); - $stmt = db()->prepare("SELECT 'payment' as trans_type, p.id, p.payment_date as trans_date, p.amount, p.payment_method, p.invoice_id as ref_no - FROM payments p - JOIN invoices i ON p.invoice_id = i.id - WHERE i.customer_id = ? AND i.type = ? AND p.payment_date BETWEEN ? AND ?"); - $stmt->execute([$entity_id, $invType, $start_date, $end_date]); + $stmt = db()->prepare("SELECT 'payment' as trans_type, p.id, p.payment_date as trans_date, p.amount, p.payment_method, p.$invFkColumn as ref_no + FROM $paymentTable p + JOIN $invoiceTable i ON p.$invFkColumn = i.id + WHERE i.$fkColumn = ? AND p.payment_date BETWEEN ? AND ?"); + $stmt->execute([$entity_id, $start_date, $end_date]); $payments = $stmt->fetchAll(PDO::FETCH_ASSOC); $transactions = array_merge($invoices, $payments); @@ -2465,15 +2519,14 @@ switch ($page) { $data['users'] = db()->query("SELECT id, username FROM users ORDER BY username ASC")->fetchAll(); break; default: - $data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll(); - // Dashboard stats + $data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll(); $data['stats'] = [ - 'total_customers' => db()->query("SELECT COUNT(*) FROM customers WHERE type = 'customer'")->fetchColumn(), + 'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(), 'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(), - 'total_sales' => db()->query("SELECT SUM(total_with_vat) FROM invoices WHERE type = 'sale'")->fetchColumn() ?: 0, - '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, + 'total_sales' => db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0, + 'total_received' => db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0, + 'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases")->fetchColumn() ?: 0, + 'total_paid' => db()->query("SELECT SUM(amount) FROM purchase_payments")->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(), 'low_stock_items_count' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level")->fetchColumn(), @@ -2482,8 +2535,8 @@ switch ($page) { $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); + $data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices 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 GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); break; } @@ -2810,6 +2863,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';