diff --git a/complete_schema.sql b/complete_schema.sql new file mode 100644 index 0000000..fbe6e17 --- /dev/null +++ b/complete_schema.sql @@ -0,0 +1,891 @@ +/*M!999999\- enable the sandbox mode */ + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- Auto-generated full schema snapshot for fresh installs. +-- Re-run refresh_complete_schema.php after schema changes so new installations stay current. + +-- +-- Table structure for table `acc_accounts` +-- +CREATE TABLE IF NOT EXISTS `acc_accounts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `code` varchar(20) NOT NULL, + `name_en` varchar(100) NOT NULL, + `name_ar` varchar(100) NOT NULL, + `type` enum('asset','liability','equity','revenue','expense') NOT NULL, + `parent_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `code` (`code`), + KEY `parent_id` (`parent_id`), + CONSTRAINT `acc_accounts_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `acc_accounts` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `acc_journal_entries` +-- +CREATE TABLE IF NOT EXISTS `acc_journal_entries` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `entry_date` date NOT NULL, + `description` text DEFAULT NULL, + `reference` varchar(100) DEFAULT NULL, + `source_type` varchar(50) DEFAULT NULL, + `source_id` int(11) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `acc_ledger` +-- +CREATE TABLE IF NOT EXISTS `acc_ledger` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `journal_entry_id` int(11) NOT NULL, + `account_id` int(11) NOT NULL, + `debit` decimal(15,3) DEFAULT 0.000, + `credit` decimal(15,3) DEFAULT 0.000, + PRIMARY KEY (`id`), + KEY `journal_entry_id` (`journal_entry_id`), + KEY `account_id` (`account_id`), + CONSTRAINT `acc_ledger_ibfk_1` FOREIGN KEY (`journal_entry_id`) REFERENCES `acc_journal_entries` (`id`) ON DELETE CASCADE, + CONSTRAINT `acc_ledger_ibfk_2` FOREIGN KEY (`account_id`) REFERENCES `acc_accounts` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `cash_registers` +-- +CREATE TABLE IF NOT EXISTS `cash_registers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `customers` +-- +CREATE TABLE IF NOT EXISTS `customers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `outlet_id` int(11) DEFAULT NULL, + `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 0.000, + `credit_limit` decimal(15,3) DEFAULT 0.000, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `loyalty_points` decimal(15,3) DEFAULT 0.000, + `loyalty_tier` enum('bronze','silver','gold') DEFAULT 'bronze', + `total_spent` decimal(15,3) DEFAULT 0.000, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- +-- Table structure for table `discount_codes` +-- +CREATE TABLE IF NOT EXISTS `discount_codes` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `code` varchar(50) NOT NULL, + `type` enum('percentage','fixed') NOT NULL DEFAULT 'percentage', + `value` decimal(15,3) NOT NULL, + `min_purchase` decimal(15,3) DEFAULT 0.000, + `expiry_date` date DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `expense_categories` +-- +CREATE TABLE IF NOT EXISTS `expense_categories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name_en` varchar(255) NOT NULL, + `name_ar` varchar(255) NOT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_biometric_devices` +-- +CREATE TABLE IF NOT EXISTS `hr_biometric_devices` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `device_name` varchar(100) NOT NULL, + `ip_address` varchar(50) NOT NULL, + `port` int(11) DEFAULT 4370, + `io_address` varchar(100) DEFAULT NULL, + `serial_number` varchar(100) DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `last_sync` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_departments` +-- +CREATE TABLE IF NOT EXISTS `hr_departments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_employees` +-- +CREATE TABLE IF NOT EXISTS `hr_employees` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `department_id` int(11) DEFAULT NULL, + `biometric_id` varchar(50) DEFAULT NULL, + `name` varchar(255) NOT NULL, + `email` varchar(255) DEFAULT NULL, + `phone` varchar(20) DEFAULT NULL, + `position` varchar(100) DEFAULT NULL, + `salary` decimal(15,3) DEFAULT 0.000, + `joining_date` date DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `biometric_id` (`biometric_id`), + KEY `department_id` (`department_id`), + CONSTRAINT `hr_employees_ibfk_1` FOREIGN KEY (`department_id`) REFERENCES `hr_departments` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_attendance` +-- +CREATE TABLE IF NOT EXISTS `hr_attendance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `employee_id` int(11) NOT NULL, + `attendance_date` date NOT NULL, + `clock_in` time DEFAULT NULL, + `clock_out` time DEFAULT NULL, + `status` enum('present','absent','on_leave') DEFAULT 'present', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `employee_id` (`employee_id`,`attendance_date`), + CONSTRAINT `hr_attendance_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `hr_employees` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_biometric_logs` +-- +CREATE TABLE IF NOT EXISTS `hr_biometric_logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `biometric_id` varchar(50) NOT NULL, + `device_id` int(11) DEFAULT NULL, + `employee_id` int(11) DEFAULT NULL, + `timestamp` datetime NOT NULL, + `type` enum('check_in','check_out','unknown') DEFAULT 'unknown', + `processed` tinyint(1) DEFAULT 0, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `employee_id` (`employee_id`), + KEY `device_id` (`device_id`), + CONSTRAINT `hr_biometric_logs_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `hr_employees` (`id`) ON DELETE SET NULL, + CONSTRAINT `hr_biometric_logs_ibfk_2` FOREIGN KEY (`device_id`) REFERENCES `hr_biometric_devices` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `hr_payroll` +-- +CREATE TABLE IF NOT EXISTS `hr_payroll` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `employee_id` int(11) NOT NULL, + `payroll_month` int(11) NOT NULL, + `payroll_year` int(11) NOT NULL, + `basic_salary` decimal(15,3) DEFAULT 0.000, + `bonus` decimal(15,3) DEFAULT 0.000, + `deductions` decimal(15,3) DEFAULT 0.000, + `net_salary` decimal(15,3) DEFAULT 0.000, + `payment_date` date DEFAULT NULL, + `status` enum('pending','paid') DEFAULT 'pending', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `employee_month_year` (`employee_id`,`payroll_month`,`payroll_year`), + CONSTRAINT `hr_payroll_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `hr_employees` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `licenses` +-- +CREATE TABLE IF NOT EXISTS `licenses` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `license_key` varchar(255) NOT NULL, + `max_activations` int(11) DEFAULT 1, + `max_counters` int(11) DEFAULT 1, + `status` enum('active','suspended','expired') DEFAULT 'active', + `owner` varchar(255) DEFAULT NULL, + `address` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `license_key` (`license_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `activations` +-- +CREATE TABLE IF NOT EXISTS `activations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `license_id` int(11) NOT NULL, + `fingerprint` varchar(255) NOT NULL, + `domain` varchar(255) DEFAULT NULL, + `product` varchar(255) DEFAULT NULL, + `activated_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `license_id` (`license_id`,`fingerprint`), + CONSTRAINT `activations_ibfk_1` FOREIGN KEY (`license_id`) REFERENCES `licenses` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `loyalty_transactions` +-- +CREATE TABLE IF NOT EXISTS `loyalty_transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `customer_id` int(11) NOT NULL, + `transaction_id` int(11) DEFAULT NULL, + `points_change` decimal(15,3) NOT NULL, + `transaction_type` enum('earned','redeemed','adjustment') NOT NULL, + `description` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `customer_id` (`customer_id`), + CONSTRAINT `loyalty_transactions_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `migrations` +-- +CREATE TABLE IF NOT EXISTS `migrations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `migration` varchar(255) NOT NULL, + `executed_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `outlets` +-- +CREATE TABLE IF NOT EXISTS `outlets` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `address` text DEFAULT NULL, + `phone` varchar(50) DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `expenses` +-- +CREATE TABLE IF NOT EXISTS `expenses` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `category_id` int(11) NOT NULL, + `amount` decimal(10,3) NOT NULL, + `expense_date` date NOT NULL, + `description` text DEFAULT NULL, + `reference_no` varchar(50) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `category_id` (`category_id`), + KEY `fk_expenses_outlet` (`outlet_id`), + CONSTRAINT `expenses_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `expense_categories` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_expenses_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `invoices` +-- +CREATE TABLE IF NOT EXISTS `invoices` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `transaction_no` varchar(50) DEFAULT NULL, + `customer_id` int(11) DEFAULT NULL, + `invoice_date` date NOT NULL, + `type` enum('sale','purchase') NOT NULL, + `payment_type` varchar(100) DEFAULT NULL, + `total_amount` decimal(15,3) DEFAULT 0.000, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `vat_amount` decimal(15,3) DEFAULT 0.000, + `total_with_vat` decimal(15,3) DEFAULT 0.000, + `terms_conditions` text DEFAULT NULL, + `paid_amount` decimal(15,3) DEFAULT 0.000, + `status` enum('paid','unpaid','partially_paid') DEFAULT 'unpaid', + `register_session_id` int(11) DEFAULT NULL, + `is_pos` tinyint(1) DEFAULT 0, + `discount_amount` decimal(15,3) DEFAULT 0.000, + `loyalty_points_earned` decimal(15,3) DEFAULT 0.000, + `loyalty_points_redeemed` decimal(15,3) DEFAULT 0.000, + `created_by` int(11) DEFAULT NULL, + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `customer_id` (`customer_id`), + KEY `idx_transaction_no` (`transaction_no`), + KEY `idx_register_session` (`register_session_id`), + KEY `fk_invoices_outlet` (`outlet_id`), + CONSTRAINT `fk_invoices_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `invoices_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `lpos` +-- +CREATE TABLE IF NOT EXISTS `lpos` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `supplier_id` int(11) NOT NULL, + `lpo_date` date NOT NULL, + `delivery_date` date DEFAULT NULL, + `status` enum('pending','converted','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, + `terms_conditions` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `supplier_id` (`supplier_id`), + KEY `fk_lpos_outlet` (`outlet_id`), + CONSTRAINT `fk_lpos_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `lpos_ibfk_1` FOREIGN KEY (`supplier_id`) REFERENCES `customers` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `payments` +-- +CREATE TABLE IF NOT EXISTS `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `outlet_id` int(11) DEFAULT NULL, + `invoice_id` int(11) NOT NULL, + `payment_date` date NOT NULL, + `amount` decimal(15,3) DEFAULT 0.000, + `payment_method` varchar(50) DEFAULT 'Cash', + `notes` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `invoice_id` (`invoice_id`), + CONSTRAINT `payments_ibfk_1` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `payment_methods` +-- +CREATE TABLE IF NOT EXISTS `payment_methods` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name_en` varchar(255) DEFAULT NULL, + `name_ar` varchar(255) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `pos_devices` +-- +CREATE TABLE IF NOT EXISTS `pos_devices` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `device_name` varchar(100) NOT NULL, + `device_type` enum('scale','printer','display') DEFAULT 'scale', + `connection_type` enum('usb','network','serial') DEFAULT 'usb', + `ip_address` varchar(50) DEFAULT NULL, + `port` int(11) DEFAULT NULL, + `baud_rate` int(11) DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `pos_held_carts` +-- +CREATE TABLE IF NOT EXISTS `pos_held_carts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `cart_name` varchar(100) NOT NULL, + `items_json` text NOT NULL, + `customer_id` int(11) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `customer_id` (`customer_id`), + KEY `fk_pos_carts_outlet` (`outlet_id`), + CONSTRAINT `fk_pos_carts_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `pos_held_carts_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `pos_transactions` +-- +CREATE TABLE IF NOT EXISTS `pos_transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `transaction_no` varchar(50) NOT NULL, + `customer_id` int(11) DEFAULT NULL, + `total_amount` decimal(15,3) NOT NULL, + `tax_amount` decimal(15,3) DEFAULT 0.000, + `discount_code_id` int(11) DEFAULT NULL, + `discount_amount` decimal(15,3) DEFAULT 0.000, + `loyalty_points_earned` decimal(15,3) DEFAULT 0.000, + `loyalty_points_redeemed` decimal(15,3) DEFAULT 0.000, + `net_amount` decimal(15,3) NOT NULL DEFAULT 0.000, + `payment_method` varchar(100) DEFAULT NULL, + `status` enum('completed','refunded','cancelled') DEFAULT 'completed', + `created_at` timestamp NULL DEFAULT current_timestamp(), + `created_by` int(11) DEFAULT NULL, + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transaction_no` (`transaction_no`), + KEY `fk_pos_trans_outlet` (`outlet_id`), + CONSTRAINT `fk_pos_trans_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `pos_items` +-- +CREATE TABLE IF NOT EXISTS `pos_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `transaction_id` int(11) NOT NULL, + `product_id` int(11) NOT NULL, + `quantity` decimal(15,3) NOT NULL, + `unit_price` decimal(15,3) NOT NULL, + `subtotal` decimal(15,3) NOT NULL, + PRIMARY KEY (`id`), + KEY `transaction_id` (`transaction_id`), + CONSTRAINT `pos_items_ibfk_1` FOREIGN KEY (`transaction_id`) REFERENCES `pos_transactions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `pos_payments` +-- +CREATE TABLE IF NOT EXISTS `pos_payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `transaction_id` int(11) NOT NULL, + `payment_method` varchar(50) NOT NULL, + `amount` decimal(15,3) NOT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `transaction_id` (`transaction_id`), + CONSTRAINT `pos_payments_ibfk_1` FOREIGN KEY (`transaction_id`) REFERENCES `pos_transactions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `purchases` +-- +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(), + `outlet_id` int(11) DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- +-- Table structure for table `purchase_items` +-- +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 COLLATE=utf8mb4_general_ci; + +-- +-- Table structure for table `purchase_payments` +-- +CREATE TABLE IF NOT EXISTS `purchase_payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `outlet_id` int(11) DEFAULT NULL, + `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 COLLATE=utf8mb4_general_ci; + +-- +-- Table structure for table `quotations` +-- +CREATE TABLE IF NOT EXISTS `quotations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `customer_id` int(11) DEFAULT NULL, + `quotation_date` date NOT NULL, + `valid_until` date DEFAULT NULL, + `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, + `terms_conditions` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `customer_id` (`customer_id`), + KEY `fk_quotations_outlet` (`outlet_id`), + CONSTRAINT `fk_quotations_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `quotations_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `register_sessions` +-- +CREATE TABLE IF NOT EXISTS `register_sessions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `register_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `opening_balance` decimal(15,3) NOT NULL, + `closing_balance` decimal(15,3) DEFAULT NULL, + `cash_in_hand` decimal(15,3) DEFAULT NULL, + `opened_at` timestamp NULL DEFAULT current_timestamp(), + `closed_at` timestamp NULL DEFAULT NULL, + `status` enum('open','closed') DEFAULT 'open', + `notes` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `role_groups` +-- +CREATE TABLE IF NOT EXISTS `role_groups` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `permissions` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `role_permissions` +-- +CREATE TABLE IF NOT EXISTS `role_permissions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `role_id` int(11) NOT NULL, + `permission` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `role_id` (`role_id`,`permission`), + CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role_groups` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `sales_returns` +-- +CREATE TABLE IF NOT EXISTS `sales_returns` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `invoice_id` int(11) NOT NULL, + `customer_id` int(11) DEFAULT NULL, + `return_date` date NOT NULL, + `total_amount` decimal(15,3) NOT NULL DEFAULT 0.000, + `notes` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `invoice_id` (`invoice_id`), + KEY `customer_id` (`customer_id`), + KEY `fk_sales_returns_outlet` (`outlet_id`), + CONSTRAINT `fk_sales_returns_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `sales_returns_ibfk_1` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON DELETE CASCADE, + CONSTRAINT `sales_returns_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `settings` +-- +CREATE TABLE IF NOT EXISTS `settings` ( + `key` varchar(50) NOT NULL, + `value` text DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `stock_categories` +-- +CREATE TABLE IF NOT EXISTS `stock_categories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name_en` varchar(255) NOT NULL, + `name_ar` varchar(255) NOT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_categories_outlet` (`outlet_id`), + CONSTRAINT `fk_categories_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `stock_units` +-- +CREATE TABLE IF NOT EXISTS `stock_units` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name_en` varchar(255) NOT NULL, + `name_ar` varchar(255) NOT NULL, + `short_name_en` varchar(50) NOT NULL, + `short_name_ar` varchar(50) NOT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_units_outlet` (`outlet_id`), + CONSTRAINT `fk_units_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `suppliers` +-- +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, + `outlet_id` int(11) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_suppliers_outlet` (`outlet_id`), + CONSTRAINT `fk_suppliers_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- +-- Table structure for table `purchase_returns` +-- +CREATE TABLE IF NOT EXISTS `purchase_returns` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `invoice_id` int(11) NOT NULL, + `supplier_id` int(11) DEFAULT NULL, + `return_date` date NOT NULL, + `total_amount` decimal(15,3) NOT NULL DEFAULT 0.000, + `notes` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `outlet_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `invoice_id` (`invoice_id`), + KEY `supplier_id` (`supplier_id`), + KEY `fk_purchase_returns_outlet` (`outlet_id`), + CONSTRAINT `fk_purchase_returns_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE SET NULL, + CONSTRAINT `purchase_returns_ibfk_1` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON DELETE CASCADE, + CONSTRAINT `purchase_returns_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `stock_items` +-- +CREATE TABLE IF NOT EXISTS `stock_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `category_id` int(11) DEFAULT NULL, + `unit_id` int(11) DEFAULT NULL, + `supplier_id` int(11) DEFAULT NULL, + `name_en` varchar(255) NOT NULL, + `name_ar` varchar(255) NOT NULL, + `sku` varchar(100) DEFAULT NULL, + `purchase_price` decimal(15,3) DEFAULT 0.000, + `sale_price` decimal(15,3) DEFAULT 0.000, + `stock_quantity` decimal(15,2) DEFAULT 0.00, + `min_stock_level` decimal(15,2) DEFAULT 0.00, + `expiry_date` date DEFAULT NULL, + `image_path` varchar(255) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `vat_rate` decimal(5,2) DEFAULT 0.00, + `is_promotion` tinyint(1) DEFAULT 0, + `promotion_start` date DEFAULT NULL, + `promotion_end` date DEFAULT NULL, + `promotion_percent` decimal(5,2) DEFAULT 0.00, + `outlet_id` int(11) DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_sku_outlet` (`sku`,`outlet_id`), + KEY `category_id` (`category_id`), + KEY `unit_id` (`unit_id`), + KEY `fk_stock_items_supplier` (`supplier_id`), + KEY `fk_items_outlet` (`outlet_id`), + CONSTRAINT `fk_items_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_stock_items_supplier` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL, + CONSTRAINT `stock_items_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `stock_categories` (`id`) ON DELETE SET NULL, + CONSTRAINT `stock_items_ibfk_2` FOREIGN KEY (`unit_id`) REFERENCES `stock_units` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `invoice_items` +-- +CREATE TABLE IF NOT EXISTS `invoice_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `invoice_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,2) NOT NULL, + `unit_price` decimal(15,3) DEFAULT 0.000, + `vat_amount` decimal(15,3) DEFAULT 0.000, + `total_price` decimal(15,3) DEFAULT 0.000, + PRIMARY KEY (`id`), + KEY `invoice_id` (`invoice_id`), + KEY `item_id` (`item_id`), + CONSTRAINT `invoice_items_ibfk_1` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON DELETE CASCADE, + CONSTRAINT `invoice_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `lpo_items` +-- +CREATE TABLE IF NOT EXISTS `lpo_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `lpo_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,3) NOT NULL, + `unit_price` decimal(15,3) NOT NULL, + `vat_percentage` decimal(5,2) DEFAULT 0.00, + `vat_amount` decimal(15,3) DEFAULT 0.000, + `total_amount` decimal(15,3) DEFAULT 0.000, + PRIMARY KEY (`id`), + KEY `lpo_id` (`lpo_id`), + KEY `item_id` (`item_id`), + CONSTRAINT `lpo_items_ibfk_1` FOREIGN KEY (`lpo_id`) REFERENCES `lpos` (`id`) ON DELETE CASCADE, + CONSTRAINT `lpo_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `outlet_stock` +-- +CREATE TABLE IF NOT EXISTS `outlet_stock` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `outlet_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,2) DEFAULT 0.00, + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `unique_item_outlet` (`outlet_id`,`item_id`), + KEY `fk_outlet_stock_item` (`item_id`), + CONSTRAINT `fk_outlet_stock_item` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_outlet_stock_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `purchase_return_items` +-- +CREATE TABLE IF NOT EXISTS `purchase_return_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `return_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,2) NOT NULL, + `unit_price` decimal(15,3) NOT NULL, + `total_price` decimal(15,3) NOT NULL, + PRIMARY KEY (`id`), + KEY `return_id` (`return_id`), + KEY `item_id` (`item_id`), + CONSTRAINT `purchase_return_items_ibfk_1` FOREIGN KEY (`return_id`) REFERENCES `purchase_returns` (`id`) ON DELETE CASCADE, + CONSTRAINT `purchase_return_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `quotation_items` +-- +CREATE TABLE IF NOT EXISTS `quotation_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `quotation_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,2) NOT NULL, + `unit_price` decimal(15,3) DEFAULT 0.000, + `total_price` decimal(15,3) DEFAULT 0.000, + PRIMARY KEY (`id`), + KEY `quotation_id` (`quotation_id`), + KEY `item_id` (`item_id`), + CONSTRAINT `quotation_items_ibfk_1` FOREIGN KEY (`quotation_id`) REFERENCES `quotations` (`id`) ON DELETE CASCADE, + CONSTRAINT `quotation_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `sales_return_items` +-- +CREATE TABLE IF NOT EXISTS `sales_return_items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `return_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `quantity` decimal(15,2) NOT NULL, + `unit_price` decimal(15,3) NOT NULL, + `total_price` decimal(15,3) NOT NULL, + PRIMARY KEY (`id`), + KEY `return_id` (`return_id`), + KEY `item_id` (`item_id`), + CONSTRAINT `sales_return_items_ibfk_1` FOREIGN KEY (`return_id`) REFERENCES `sales_returns` (`id`) ON DELETE CASCADE, + CONSTRAINT `sales_return_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `stock_items` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `system_license` +-- +CREATE TABLE IF NOT EXISTS `system_license` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `license_key` varchar(255) NOT NULL, + `activation_token` text DEFAULT NULL, + `fingerprint` varchar(255) DEFAULT NULL, + `status` enum('pending','active','expired','suspended','trial') DEFAULT 'pending', + `activated_at` datetime DEFAULT NULL, + `last_checked_at` datetime DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `trial_started_at` datetime DEFAULT NULL, + `allowed_activations` int(11) DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `users` +-- +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `outlet_id` int(11) DEFAULT NULL, + `group_id` int(11) DEFAULT NULL, + `username` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `email` varchar(255) DEFAULT NULL, + `phone` varchar(20) DEFAULT NULL, + `profile_pic` varchar(255) DEFAULT NULL, + `theme` varchar(20) DEFAULT 'default', + `status` enum('active','inactive') DEFAULT 'active', + `created_at` timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`), + KEY `group_id` (`group_id`), + CONSTRAINT `users_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `role_groups` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `user_outlets` +-- +CREATE TABLE IF NOT EXISTS `user_outlets` ( + `user_id` int(11) NOT NULL, + `outlet_id` int(11) NOT NULL, + PRIMARY KEY (`user_id`,`outlet_id`), + KEY `fk_user_outlets_outlet` (`outlet_id`), + CONSTRAINT `fk_user_outlets_outlet` FOREIGN KEY (`outlet_id`) REFERENCES `outlets` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_user_outlets_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; diff --git a/cookies_debug.txt b/cookies_debug.txt new file mode 100644 index 0000000..ebe17bd --- /dev/null +++ b/cookies_debug.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +127.0.0.1 FALSE / FALSE 0 PHPSESSID 2uea9bvpufvp1th7vnnvcog2as diff --git a/cookies_verify.txt b/cookies_verify.txt new file mode 100644 index 0000000..d765b2a --- /dev/null +++ b/cookies_verify.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +127.0.0.1 FALSE / FALSE 0 PHPSESSID il48dnd8duujsd9cbumbfbum85 diff --git a/db/migrations/20260502_full_schema_sync.php b/db/migrations/20260502_full_schema_sync.php new file mode 100644 index 0000000..e29e67e --- /dev/null +++ b/db/migrations/20260502_full_schema_sync.php @@ -0,0 +1,313 @@ + [ + 'permissions' => 'TEXT NULL', + ], + 'users' => [ + 'theme' => "VARCHAR(20) DEFAULT 'default'", + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'customers' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'suppliers' => [ + 'outlet_id' => 'INT(11) DEFAULT 1', + ], + 'stock_categories' => [ + 'outlet_id' => 'INT(11) DEFAULT 1', + ], + 'stock_units' => [ + 'outlet_id' => 'INT(11) DEFAULT 1', + ], + 'stock_items' => [ + 'outlet_id' => 'INT(11) DEFAULT 1', + ], + 'expenses' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'invoices' => [ + 'transaction_no' => 'VARCHAR(50) DEFAULT NULL', + 'register_session_id' => 'INT(11) DEFAULT NULL', + 'is_pos' => 'TINYINT(1) DEFAULT 0', + 'discount_amount' => 'DECIMAL(15,3) DEFAULT 0.000', + 'loyalty_points_earned' => 'DECIMAL(15,3) DEFAULT 0.000', + 'loyalty_points_redeemed' => 'DECIMAL(15,3) DEFAULT 0.000', + 'created_by' => 'INT(11) DEFAULT NULL', + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'invoice_items' => [ + 'vat_amount' => 'DECIMAL(15,3) DEFAULT 0.000', + ], + 'payments' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'pos_held_carts' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'pos_transactions' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'purchase_payments' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'purchase_returns' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'purchases' => [ + 'outlet_id' => 'INT(11) DEFAULT 1', + ], + 'quotations' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'sales_returns' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + 'lpos' => [ + 'outlet_id' => 'INT(11) DEFAULT NULL', + ], + ]; + + foreach ($columns as $table => $tableColumns) { + if (!full_schema_sync_20260502_table_exists($pdo, $table)) { + continue; + } + + foreach ($tableColumns as $column => $definition) { + if (full_schema_sync_20260502_column_exists($pdo, $table, $column)) { + continue; + } + + $statement = sprintf( + 'ALTER TABLE `%s` ADD COLUMN `%s` %s', + str_replace('`', '', $table), + str_replace('`', '', $column), + $definition + ); + + full_schema_sync_20260502_exec($pdo, $statement); + } + } + } + + function full_schema_sync_20260502_seed_defaults(PDO $pdo): void + { + if (full_schema_sync_20260502_table_exists($pdo, 'outlets')) { + $pdo->exec( + "INSERT INTO outlets (id, name, address, phone, status, created_at) " + . "SELECT 1, 'Main Outlet', 'Head Office', '', 'active', NOW() " + . "WHERE NOT EXISTS (SELECT 1 FROM outlets WHERE id = 1)" + ); + } + + if (full_schema_sync_20260502_table_exists($pdo, 'role_groups') && full_schema_sync_20260502_column_exists($pdo, 'role_groups', 'permissions')) { + $pdo->exec("UPDATE role_groups SET permissions = 'all' WHERE permissions IS NULL AND LOWER(name) IN ('administrator', 'admin')"); + } + + if ( + full_schema_sync_20260502_table_exists($pdo, 'role_groups') + && full_schema_sync_20260502_table_exists($pdo, 'role_permissions') + ) { + $pdo->exec( + "INSERT INTO role_permissions (role_id, permission) " + . "SELECT rg.id, 'all' " + . "FROM role_groups rg " + . "LEFT JOIN role_permissions rp ON rp.role_id = rg.id AND rp.permission = 'all' " + . "WHERE LOWER(rg.name) IN ('administrator', 'admin') AND rp.id IS NULL" + ); + } + + $outletTables = [ + 'users', + 'customers', + 'suppliers', + 'stock_categories', + 'stock_units', + 'stock_items', + 'expenses', + 'invoices', + 'payments', + 'pos_held_carts', + 'pos_transactions', + 'purchase_payments', + 'purchase_returns', + 'purchases', + 'quotations', + 'sales_returns', + 'lpos', + ]; + + foreach ($outletTables as $table) { + if ( + full_schema_sync_20260502_table_exists($pdo, $table) + && full_schema_sync_20260502_column_exists($pdo, $table, 'outlet_id') + ) { + $pdo->exec(sprintf('UPDATE `%s` SET `outlet_id` = 1 WHERE `outlet_id` IS NULL', $table)); + } + } + + if ( + full_schema_sync_20260502_table_exists($pdo, 'user_outlets') + && full_schema_sync_20260502_table_exists($pdo, 'users') + && full_schema_sync_20260502_table_exists($pdo, 'outlets') + ) { + $pdo->exec( + 'INSERT INTO user_outlets (user_id, outlet_id) ' + . 'SELECT u.id, 1 ' + . 'FROM users u ' + . 'LEFT JOIN user_outlets uo ON uo.user_id = u.id AND uo.outlet_id = 1 ' + . 'WHERE uo.user_id IS NULL' + ); + } + + if ( + full_schema_sync_20260502_table_exists($pdo, 'outlet_stock') + && full_schema_sync_20260502_table_exists($pdo, 'stock_items') + && full_schema_sync_20260502_table_exists($pdo, 'outlets') + && full_schema_sync_20260502_column_exists($pdo, 'stock_items', 'stock_quantity') + ) { + $pdo->exec( + 'INSERT INTO outlet_stock (outlet_id, item_id, quantity) ' + . 'SELECT 1, si.id, COALESCE(si.stock_quantity, 0) ' + . 'FROM stock_items si ' + . 'LEFT JOIN outlet_stock os ON os.outlet_id = 1 AND os.item_id = si.id ' + . 'WHERE os.id IS NULL' + ); + } + } + + function full_schema_sync_20260502_table_exists(PDO $pdo, string $table): bool + { + $stmt = $pdo->prepare( + 'SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table LIMIT 1' + ); + $stmt->execute(['table' => $table]); + + return (bool) $stmt->fetchColumn(); + } + + function full_schema_sync_20260502_column_exists(PDO $pdo, string $table, string $column): bool + { + $stmt = $pdo->prepare( + 'SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND COLUMN_NAME = :column LIMIT 1' + ); + $stmt->execute([ + 'table' => $table, + 'column' => $column, + ]); + + return (bool) $stmt->fetchColumn(); + } + + function full_schema_sync_20260502_exec(PDO $pdo, string $statement): void + { + try { + $pdo->exec($statement); + } catch (PDOException $exception) { + if (full_schema_sync_20260502_is_ignorable($exception)) { + return; + } + + throw $exception; + } + } + + function full_schema_sync_20260502_is_ignorable(PDOException $exception): bool + { + $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; + $message = strtolower($exception->getMessage()); + $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; + $ignorableSnippets = [ + 'already exists', + 'duplicate column name', + 'duplicate key name', + 'duplicate entry', + 'duplicate foreign key constraint name', + 'duplicate key on write or update', + 'errno: 121', + 'check that column/key exists', + ]; + + if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { + return true; + } + + foreach ($ignorableSnippets as $snippet) { + if (str_contains($message, $snippet)) { + return true; + } + } + + return false; + } +} + +full_schema_sync_20260502_run(); + +return true; diff --git a/includes/DatabaseInstaller.php b/includes/DatabaseInstaller.php index fd1d414..28bf08f 100644 --- a/includes/DatabaseInstaller.php +++ b/includes/DatabaseInstaller.php @@ -1,21 +1,22 @@ query("SHOW TABLES LIKE 'users'"); - return $stmt->fetch() !== false; + return self::tableExists(db(), 'users'); } catch (PDOException $e) { return false; } } - public static function install() { + public static function install(): bool { require_once __DIR__ . '/../db/config.php'; - - $schemaFile = __DIR__ . '/../db/schema.sql'; + + $pdo = db(); + $schemaFile = self::getInstallSchemaFile(); $seedFile = __DIR__ . '/../db/seed.sql'; if (!file_exists($schemaFile)) { @@ -24,25 +25,78 @@ class DatabaseInstaller { self::executeSqlFile($schemaFile); - if (file_exists($seedFile)) { + if (file_exists($seedFile) && self::shouldImportSeed($pdo)) { self::executeSqlFile($seedFile); } + self::ensureCurrentSchema(); + return true; } - private static function executeSqlFile($filePath) { - // Use constants from db/config.php + private static function getInstallSchemaFile(): string { + $completeSchemaFile = __DIR__ . '/../complete_schema.sql'; + if (file_exists($completeSchemaFile)) { + return $completeSchemaFile; + } + + return __DIR__ . '/../db/schema.sql'; + } + + private static function shouldImportSeed(PDO $pdo): bool { + if (!self::tableExists($pdo, 'migrations')) { + return true; + } + + $stmt = $pdo->query('SELECT COUNT(*) FROM migrations'); + return (int) $stmt->fetchColumn() === 0; + } + + public static function ensureCurrentSchema(): void { + require_once __DIR__ . '/../db/config.php'; + + $pdo = db(); + if (!self::tableExists($pdo, 'users')) { + return; + } + + self::ensureMigrationsTable($pdo); + $executed = self::getExecutedMigrations($pdo); + + foreach (self::getMigrationFiles() as $filePath) { + $migrationName = basename($filePath); + if (isset($executed[$migrationName])) { + continue; + } + + $extension = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION)); + if ($extension === 'sql') { + self::executeSqlMigration($pdo, $filePath); + } elseif ($extension === 'php') { + self::executePhpMigration($filePath); + } else { + throw new RuntimeException("Unsupported migration type: $migrationName"); + } + + self::recordMigration($pdo, $migrationName); + $executed[$migrationName] = true; + } + } + + private static function executeSqlFile(string $filePath): void { + require_once __DIR__ . '/../db/config.php'; + $host = DB_HOST; $name = DB_NAME; $user = DB_USER; $pass = DB_PASS; + $passwordSegment = $pass !== '' ? ' -p' . escapeshellarg($pass) : ''; $command = sprintf( - 'mysql -h %s -u %s -p%s %s < %s', + 'mysql -h %s -u %s%s %s < %s', escapeshellarg($host), escapeshellarg($user), - escapeshellarg($pass), + $passwordSegment, escapeshellarg($name), escapeshellarg($filePath) ); @@ -52,7 +106,265 @@ class DatabaseInstaller { exec($command . ' 2>&1', $output, $returnVar); if ($returnVar !== 0) { - throw new Exception("Failed to execute SQL file: " . implode("\n", $output)); + throw new Exception('Failed to execute SQL file: ' . implode("\n", $output)); } } + + private static function ensureMigrationsTable(PDO $pdo): void { + $pdo->exec( + "CREATE TABLE IF NOT EXISTS migrations (\n" + . " id INT AUTO_INCREMENT PRIMARY KEY,\n" + . " migration VARCHAR(255) NOT NULL,\n" + . " executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n" + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); + } + + private static function getExecutedMigrations(PDO $pdo): array { + $stmt = $pdo->query('SELECT migration FROM migrations'); + $rows = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []; + return array_fill_keys($rows, true); + } + + private static function getMigrationFiles(): array { + $files = array_merge( + glob(__DIR__ . '/../db/migrations/*.sql') ?: [], + glob(__DIR__ . '/../db/migrations/*.php') ?: [] + ); + + usort($files, static function (string $left, string $right): int { + return strnatcasecmp(self::migrationSortKey($left), self::migrationSortKey($right)); + }); + + return $files; + } + + private static function migrationSortKey(string $filePath): string { + $basename = basename($filePath); + + return match ($basename) { + '20260318_add_outlet_id_to_purchases.sql' => '20260318_10_add_outlet_id_to_purchases.sql', + '20260318_create_outlets_table.sql' => '20260318_20_create_outlets_table.sql', + '20260318_multi_outlet_schema.sql' => '20260318_30_multi_outlet_schema.sql', + '20260318_local_definitions.sql' => '20260318_40_local_definitions.sql', + '20260318_user_outlets_table.sql' => '20260318_50_user_outlets_table.sql', + default => $basename, + }; + } + + private static function executeSqlMigration(PDO $pdo, string $filePath): void { + $sql = file_get_contents($filePath); + if ($sql === false) { + throw new RuntimeException('Unable to read SQL migration: ' . basename($filePath)); + } + + $statements = self::splitSqlStatements($sql); + foreach ($statements as $index => $statement) { + try { + $pdo->exec($statement); + } catch (PDOException $exception) { + if (self::isIgnorableMigrationError($pdo, $exception, $statement)) { + continue; + } + + throw new RuntimeException( + 'SQL migration failed in ' . basename($filePath) . ' at statement ' . ($index + 1) . ': ' . $exception->getMessage(), + 0, + $exception + ); + } + } + } + + private static function executePhpMigration(string $filePath): void { + ob_start(); + + try { + $result = include $filePath; + } catch (Throwable $throwable) { + ob_end_clean(); + throw new RuntimeException('PHP migration failed in ' . basename($filePath) . ': ' . $throwable->getMessage(), 0, $throwable); + } + + ob_end_clean(); + + if ($result === false) { + throw new RuntimeException('PHP migration returned false: ' . basename($filePath)); + } + } + + private static function splitSqlStatements(string $sql): array { + $sql = preg_replace('/^\xEF\xBB\xBF/', '', $sql) ?? $sql; + $sql = preg_replace('/\/\*.*?\*\//s', '', $sql) ?? $sql; + + $lines = preg_split('/\R/', $sql) ?: []; + $filteredLines = []; + + foreach ($lines as $line) { + $trimmed = ltrim($line); + if ($trimmed === '' || str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) { + continue; + } + $filteredLines[] = $line; + } + + $cleanSql = implode("\n", $filteredLines); + $statements = []; + $buffer = ''; + $inSingleQuote = false; + $inDoubleQuote = false; + $length = strlen($cleanSql); + + for ($index = 0; $index < $length; $index++) { + $char = $cleanSql[$index]; + $previous = $index > 0 ? $cleanSql[$index - 1] : ''; + + if ($char === "'" && !$inDoubleQuote && $previous !== '\\') { + $inSingleQuote = !$inSingleQuote; + } elseif ($char === '"' && !$inSingleQuote && $previous !== '\\') { + $inDoubleQuote = !$inDoubleQuote; + } + + if ($char === ';' && !$inSingleQuote && !$inDoubleQuote) { + $statement = trim($buffer); + if ($statement !== '') { + $statements[] = $statement; + } + $buffer = ''; + continue; + } + + $buffer .= $char; + } + + $tail = trim($buffer); + if ($tail !== '') { + $statements[] = $tail; + } + + return $statements; + } + + private static function tableExists(PDO $pdo, string $tableName): bool { + static $cache = []; + + $normalized = strtolower($tableName); + if (array_key_exists($normalized, $cache)) { + return $cache[$normalized]; + } + + $stmt = $pdo->prepare('SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table LIMIT 1'); + $stmt->execute(['table' => $tableName]); + + $cache[$normalized] = (bool) $stmt->fetchColumn(); + return $cache[$normalized]; + } + + private static function schemaDefinesTable(string $tableName): bool { + static $tables = null; + + if ($tables === null) { + $tables = []; + $schemaFiles = [ + __DIR__ . '/../db/schema.sql', + __DIR__ . '/../complete_schema.sql', + ]; + + foreach ($schemaFiles as $schemaFile) { + if (!is_file($schemaFile)) { + continue; + } + + $sql = file_get_contents($schemaFile); + if ($sql === false) { + continue; + } + + if (preg_match_all('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $sql, $matches)) { + foreach ($matches[1] as $name) { + $tables[strtolower($name)] = true; + } + } + } + } + + return isset($tables[strtolower($tableName)]); + } + + private static function extractMissingTableName(PDOException $exception): ?string { + $message = $exception->getMessage(); + + if (preg_match("/Table '([^']+)' doesn't exist/i", $message, $matches)) { + $qualifiedName = str_replace('`', '', $matches[1]); + $parts = explode('.', $qualifiedName); + $tableName = trim((string) end($parts)); + return $tableName !== '' ? $tableName : null; + } + + return null; + } + + private static function statementMentionsTable(string $statement, string $tableName): bool { + $pattern = '/(^|[^a-zA-Z0-9_])`?' . preg_quote($tableName, '/') . '`?([^a-zA-Z0-9_]|$)/i'; + return preg_match($pattern, $statement) === 1; + } + + private static function isLegacyMissingTableError(PDO $pdo, PDOException $exception, string $statement): bool { + $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; + $message = strtolower($exception->getMessage()); + + if ($driverCode !== 1146 && !str_contains($message, 'base table or view not found')) { + return false; + } + + $missingTable = self::extractMissingTableName($exception); + if ($missingTable === null || !self::statementMentionsTable($statement, $missingTable)) { + return false; + } + + if (self::tableExists($pdo, $missingTable) || self::schemaDefinesTable($missingTable)) { + return false; + } + + return true; + } + + private static function isIgnorableMigrationError(PDO $pdo, PDOException $exception, string $statement): bool { + $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; + $message = strtolower($exception->getMessage()); + $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; + $ignorableSnippets = [ + 'already exists', + 'duplicate column name', + 'duplicate key name', + 'duplicate entry', + 'duplicate foreign key constraint name', + 'duplicate key on write or update', + 'errno: 121', + 'check that column/key exists', + ]; + + if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { + return true; + } + + foreach ($ignorableSnippets as $snippet) { + if (str_contains($message, $snippet)) { + return true; + } + } + + return self::isLegacyMissingTableError($pdo, $exception, $statement); + } + + private static function recordMigration(PDO $pdo, string $migrationName): void { + $check = $pdo->prepare('SELECT 1 FROM migrations WHERE migration = ? LIMIT 1'); + $check->execute([$migrationName]); + if ($check->fetchColumn()) { + return; + } + + $stmt = $pdo->prepare('INSERT INTO migrations (migration) VALUES (?)'); + $stmt->execute([$migrationName]); + } } diff --git a/index.php b/index.php index 4bdd026..147d98f 100644 --- a/index.php +++ b/index.php @@ -61,17 +61,44 @@ if (!function_exists('current_outlet_id')) { } } +if (!function_exists('db_table_exists')) { + function db_table_exists(string $tableName): bool { + static $cache = []; + + $normalized = strtolower($tableName); + if (array_key_exists($normalized, $cache)) { + return $cache[$normalized]; + } + + try { + $stmt = db()->prepare("SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1"); + $stmt->execute([$tableName]); + $cache[$normalized] = (bool)$stmt->fetchColumn(); + } catch (Throwable $e) { + $cache[$normalized] = false; + } + + return $cache[$normalized]; + } +} + // Handle Outlet Switch if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) { $target_id = (int)$_GET['id']; $allowed_outlets = $_SESSION['user_outlets'] ?? [1]; $is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator'; - if ($target_id === -1) { $_SESSION['outlet_id'] = -1; } elseif ($is_admin || in_array($target_id, $allowed_outlets)) { - $stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'"); - $stmt->execute([$target_id]); - if ($stmt->fetchColumn()) { + if ($target_id === -1) { + $_SESSION['outlet_id'] = -1; + } elseif ($is_admin || in_array($target_id, $allowed_outlets)) { + if (!db_table_exists('outlets')) { $_SESSION['outlet_id'] = $target_id; + } else { + $stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'"); + $stmt->execute([$target_id]); + if ($stmt->fetchColumn()) { + $_SESSION['outlet_id'] = $target_id; + } } } header("Location: index.php"); @@ -92,13 +119,14 @@ try { require_once 'includes/DatabaseInstaller.php'; -// Auto-install database if not installed -if (!DatabaseInstaller::isInstalled()) { - try { +// Auto-install database if not installed, then ensure pending migrations are applied. +try { + if (!DatabaseInstaller::isInstalled()) { DatabaseInstaller::install(); - } catch (Exception $e) { - die("Installation Error: " . $e->getMessage()); } + DatabaseInstaller::ensureCurrentSchema(); +} catch (Exception $e) { + die("Installation Error: " . $e->getMessage()); } require_once 'lib/LicenseService.php'; @@ -368,48 +396,65 @@ function renderPagination($currentPage, $totalPages) { // Login Logic $login_error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) { - $user = $_POST['username'] ?? ''; - $pass = $_POST['password'] ?? ''; - $stmt = db()->prepare("SELECT u.*, g.name as role_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id WHERE u.username = ? AND u.status = 'active'"); - $stmt->execute([$user]); - $u = $stmt->fetch(); - if ($u && password_verify($pass, $u['password'])) { - $_SESSION['user_id'] = $u['id']; - $_SESSION['username'] = $u['username']; - $_SESSION['user_role_name'] = $u['role_name']; - - // Fetch permissions from the new role_permissions table - $permStmt = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?"); - $permStmt->execute([$u['group_id']]); - $permissions = $permStmt->fetchAll(PDO::FETCH_COLUMN); - $_SESSION['user_permissions'] = $permissions; - - $_SESSION['profile_pic'] = $u['profile_pic']; - $_SESSION['theme'] = $u['theme'] ?? 'default'; + $user = trim((string)($_POST['username'] ?? '')); + $pass = (string)($_POST['password'] ?? ''); - // --- Multi-Outlet Login Logic --- - // Fetch assigned outlets - $outletStmt = db()->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?"); - $outletStmt->execute([$u['id']]); - $user_outlets = $outletStmt->fetchAll(PDO::FETCH_COLUMN); - - if (empty($user_outlets)) { - if (($u['role_name'] ?? '') === 'Administrator') { - $allOutlets = db()->query("SELECT id FROM outlets WHERE status = 'active'")->fetchAll(PDO::FETCH_COLUMN); - $user_outlets = $allOutlets ?: [1]; - } else { - $user_outlets = [1]; + try { + $stmt = db()->prepare("SELECT u.*, g.name as role_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id WHERE u.username = ? AND u.status = 'active'"); + $stmt->execute([$user]); + $u = $stmt->fetch(); + + if ($u && password_verify($pass, (string)($u['password'] ?? ''))) { + $_SESSION['user_id'] = $u['id']; + $_SESSION['username'] = $u['username']; + $_SESSION['user_role_name'] = $u['role_name']; + + $permissions = []; + if (db_table_exists('role_permissions')) { + $permStmt = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?"); + $permStmt->execute([$u['group_id']]); + $permissions = $permStmt->fetchAll(PDO::FETCH_COLUMN) ?: []; } + $_SESSION['user_permissions'] = $permissions; + + $_SESSION['profile_pic'] = $u['profile_pic'] ?? null; + $_SESSION['theme'] = $u['theme'] ?? 'default'; + + $user_outlets = []; + if (db_table_exists('user_outlets')) { + $outletStmt = db()->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?"); + $outletStmt->execute([$u['id']]); + $user_outlets = $outletStmt->fetchAll(PDO::FETCH_COLUMN) ?: []; + } + + if (empty($user_outlets)) { + if (($u['role_name'] ?? '') === 'Administrator' && db_table_exists('outlets')) { + $allOutlets = db()->query("SELECT id FROM outlets WHERE status = 'active'")->fetchAll(PDO::FETCH_COLUMN); + $user_outlets = $allOutlets ?: [1]; + } else { + $user_outlets = [1]; + } + } + + $user_outlets = array_values(array_unique(array_map('intval', $user_outlets))); + if ($user_outlets === []) { + $user_outlets = [1]; + } + + $_SESSION['user_outlets'] = $user_outlets; + $_SESSION['outlet_id'] = $user_outlets[0]; + header("Location: index.php"); + exit; } - $_SESSION['user_outlets'] = $user_outlets; - $_SESSION['outlet_id'] = $user_outlets[0]; - header("Location: index.php"); - exit; - } else { + $login_error = "Invalid username or password"; - // Debugging $reason = (!$u) ? "User not found or inactive" : "Password mismatch"; file_put_contents('login_debug.log', date('Y-m-d H:i:s') . " - Failed login for '$user'. Reason: $reason\n", FILE_APPEND); + } catch (Throwable $e) { + file_put_contents('login_debug.log', date('Y-m-d H:i:s') . " - Login exception for '$user': " . $e->getMessage() . "\n", FILE_APPEND); + $login_error = $lang === 'ar' + ? 'تعذر تسجيل الدخول مؤقتًا. يرجى تحديث الصفحة والمحاولة مرة أخرى.' + : 'Sign-in is temporarily unavailable. Please refresh the page and try again.'; } } @@ -834,14 +879,13 @@ if (!isset($_SESSION['user_id'])) {
- +
- +
-
diff --git a/installation/index.php b/installation/index.php index b066075..cfa0d2d 100644 --- a/installation/index.php +++ b/installation/index.php @@ -100,100 +100,10 @@ if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') { $adminEmail = $_POST['admin_email'] ?? ''; try { + require_once __DIR__ . '/../includes/DatabaseInstaller.php'; + DatabaseInstaller::install(); $pdo = db(); - - // Import full schema if available - $schemaFile = __DIR__ . '/../complete_schema.sql'; - if (file_exists($schemaFile)) { - $host = DB_HOST; - $name = DB_NAME; - $user = DB_USER; - $pass = DB_PASS; - - // Use mysql CLI for reliable import - $cmd = sprintf( - 'mysql -h %s -u %s %s %s < %s', - escapeshellarg($host), - escapeshellarg($user), - ($pass ? '-p' . escapeshellarg($pass) : ''), - escapeshellarg($name), - escapeshellarg($schemaFile) - ); - - exec($cmd, $output, $returnVar); - - if ($returnVar !== 0) { - // Fallback to manual execution if CLI fails - $sql = file_get_contents($schemaFile); - $pdo->exec($sql); - } - } else { - // Fallback to base tables if complete_schema.sql is missing - $pdo->exec("CREATE TABLE IF NOT EXISTS role_groups ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"); - $pdo->exec("CREATE TABLE IF NOT EXISTS role_permissions ( - id INT AUTO_INCREMENT PRIMARY KEY, - role_id INT NOT NULL, - permission VARCHAR(255) NOT NULL, - UNIQUE KEY (role_id, permission), - FOREIGN KEY (role_id) REFERENCES role_groups(id) ON DELETE CASCADE - )"); - - $pdo->exec("CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - email VARCHAR(100), - phone VARCHAR(20), - group_id INT, - status ENUM('active', 'inactive') DEFAULT 'active', - profile_pic VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"); - } - - // --- Run Migrations --- - $stmt = $pdo->query("SHOW TABLES LIKE 'migrations'"); - if ($stmt->rowCount() == 0) { - $pdo->exec("CREATE TABLE migrations ( - id INT AUTO_INCREMENT PRIMARY KEY, - migration VARCHAR(255) NOT NULL, - executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"); - } - - $stmt = $pdo->query("SELECT migration FROM migrations"); - $executed = $stmt->fetchAll(PDO::FETCH_COLUMN); - - $files = installationMigrationFiles(); - foreach ($files as $file) { - $migrationName = basename($file); - if (!in_array($migrationName, $executed, true)) { - try { - $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - if ($extension === 'php') { - include $file; - } else { - $sql = file_get_contents($file); - if ($sql === false) { - throw new RuntimeException('Unable to read migration file: ' . $migrationName); - } - $pdo->exec($sql); - } - - $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); - $stmt->execute([$migrationName]); - $executed[] = $migrationName; - } catch (Throwable $e) { - // Ignore migration errors here to keep setup resilient when schema/seed already contain parts of the change. - } - } - } - // --- End Migrations --- // Ensure Admin Role exists (might be in schema but just in case) $stmt = $pdo->prepare("SELECT id FROM role_groups WHERE name = 'Administrator'"); $stmt->execute(); diff --git a/post_debug.log b/post_debug.log index f7c79ad..e58f560 100644 --- a/post_debug.log +++ b/post_debug.log @@ -145,3 +145,6 @@ 2026-04-09 04:06:58 - POST: {"username":"admin","password":"admin","login":""} 2026-04-09 04:08:44 - POST: {"username":"admin","password":"admin","login":"1"} 2026-04-09 04:46:43 - POST: {"username":"admin","password":"admin","login":""} +2026-05-01 19:22:46 - POST: {"start_trial":"1"} +2026-05-01 19:22:46 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-01 19:32:47 - POST: {"username":"admin","password":"admin","login":"1"} diff --git a/refresh_complete_schema.php b/refresh_complete_schema.php new file mode 100644 index 0000000..7d7abc4 --- /dev/null +++ b/refresh_complete_schema.php @@ -0,0 +1,308 @@ + 0, + 'path' => '/', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'None', + ]); + } +} + +function canRefreshSchemaSnapshot(): bool +{ + if (PHP_SAPI === 'cli') { + return true; + } + + configureSnapshotSession(); + + if (session_status() === PHP_SESSION_NONE) { + @session_start(); + } + + $roleName = (string) ($_SESSION['user_role_name'] ?? ''); + if (strcasecmp($roleName, 'Administrator') === 0 || (int) ($_SESSION['user_id'] ?? 0) === 1) { + return true; + } + + $remoteAddress = $_SERVER['REMOTE_ADDR'] ?? ''; + return in_array($remoteAddress, ['127.0.0.1', '::1'], true); +} + +function quoteIdentifier(string $identifier): string +{ + return '`' . str_replace('`', '``', $identifier) . '`'; +} + +function normalizeCreateTableSql(string $sql): string +{ + $sql = str_replace(["\r\n", "\r"], "\n", trim($sql)); + + if (!preg_match('/^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i', $sql)) { + $sql = preg_replace('/^CREATE\s+TABLE\b/i', 'CREATE TABLE IF NOT EXISTS', $sql, 1) ?? $sql; + } + + $sql = preg_replace('/\s+AUTO_INCREMENT=\d+\b/i', '', $sql) ?? $sql; + + return rtrim($sql, ';') . ';'; +} + +function fetchBaseTableNames(PDO $pdo): array +{ + $stmt = $pdo->query("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"); + $rows = $stmt ? $stmt->fetchAll(PDO::FETCH_NUM) : []; + $tables = []; + + foreach ($rows as $row) { + if (!isset($row[0])) { + continue; + } + + $tableName = (string) $row[0]; + if ($tableName === '') { + continue; + } + + $tables[] = $tableName; + } + + usort($tables, 'strnatcasecmp'); + return $tables; +} + +function fetchCreateTableSql(PDO $pdo, string $tableName): string +{ + $stmt = $pdo->query('SHOW CREATE TABLE ' . quoteIdentifier($tableName)); + $row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : false; + + if (!$row) { + throw new RuntimeException('Unable to read CREATE TABLE statement for ' . $tableName); + } + + $createSql = $row['Create Table'] ?? null; + if (!is_string($createSql) || $createSql === '') { + $values = array_values($row); + $createSql = isset($values[1]) && is_string($values[1]) ? $values[1] : ''; + } + + if ($createSql === '') { + throw new RuntimeException('Database did not return a CREATE TABLE statement for ' . $tableName); + } + + return normalizeCreateTableSql($createSql); +} + +function extractTableDependencies(string $tableName, string $createSql): array +{ + if (!preg_match_all('/REFERENCES\s+`?([a-zA-Z0-9_]+)`?/i', $createSql, $matches)) { + return []; + } + + $dependencies = []; + $normalizedTable = strtolower($tableName); + + foreach ($matches[1] as $referencedTable) { + $referencedTable = strtolower((string) $referencedTable); + if ($referencedTable === '' || $referencedTable === $normalizedTable) { + continue; + } + + $dependencies[$referencedTable] = true; + } + + return array_keys($dependencies); +} + +function fetchTableDefinitions(PDO $pdo): array +{ + $definitions = []; + + foreach (fetchBaseTableNames($pdo) as $tableName) { + $definitions[$tableName] = fetchCreateTableSql($pdo, $tableName); + } + + if ($definitions === []) { + throw new RuntimeException('No base tables were found in the current database.'); + } + + return $definitions; +} + +function orderTablesForSnapshot(array $definitions): array +{ + $orderedNames = array_keys($definitions); + usort($orderedNames, 'strnatcasecmp'); + + $actualNamesByLower = []; + foreach ($orderedNames as $tableName) { + $actualNamesByLower[strtolower($tableName)] = $tableName; + } + + $incoming = []; + $graph = []; + foreach ($orderedNames as $tableName) { + $incoming[$tableName] = []; + $graph[$tableName] = []; + } + + foreach ($definitions as $tableName => $createSql) { + foreach (extractTableDependencies($tableName, $createSql) as $dependencyLower) { + if (!isset($actualNamesByLower[$dependencyLower])) { + continue; + } + + $dependency = $actualNamesByLower[$dependencyLower]; + $incoming[$tableName][$dependency] = true; + $graph[$dependency][$tableName] = true; + } + } + + $queue = []; + foreach ($orderedNames as $tableName) { + if ($incoming[$tableName] === []) { + $queue[] = $tableName; + } + } + usort($queue, 'strnatcasecmp'); + + $snapshotOrder = []; + while ($queue !== []) { + $tableName = array_shift($queue); + $snapshotOrder[] = $tableName; + + foreach (array_keys($graph[$tableName]) as $child) { + unset($incoming[$child][$tableName]); + if ($incoming[$child] === []) { + $queue[] = $child; + } + } + + usort($queue, 'strnatcasecmp'); + } + + if (count($snapshotOrder) < count($orderedNames)) { + $remaining = array_values(array_diff($orderedNames, $snapshotOrder)); + usort($remaining, 'strnatcasecmp'); + $snapshotOrder = array_merge($snapshotOrder, $remaining); + } + + return $snapshotOrder; +} + +function buildSnapshotContent(PDO $pdo, array &$tableOrder = []): string +{ + $definitions = fetchTableDefinitions($pdo); + $tableOrder = orderTablesForSnapshot($definitions); + + $lines = [ + '/*M!999999\\- enable the sandbox mode */ ', + '', + '/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;', + '/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;', + '/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;', + '/*!40101 SET NAMES utf8mb4 */;', + '/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;', + "/*!40103 SET TIME_ZONE='+00:00' */;", + '/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;', + '/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;', + "/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;", + '/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;', + '', + '-- Auto-generated full schema snapshot for fresh installs.', + '-- Re-run refresh_complete_schema.php after schema changes so new installations stay current.', + '', + ]; + + foreach ($tableOrder as $tableName) { + $lines[] = '--'; + $lines[] = '-- Table structure for table ' . quoteIdentifier($tableName); + $lines[] = '--'; + $lines[] = $definitions[$tableName]; + $lines[] = ''; + } + + $lines[] = '/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;'; + $lines[] = '/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;'; + $lines[] = '/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;'; + $lines[] = '/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;'; + $lines[] = '/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;'; + $lines[] = '/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;'; + $lines[] = '/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;'; + $lines[] = '/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;'; + + return implode(PHP_EOL, $lines) . PHP_EOL; +} + +function runSchemaSnapshotRefresh(): int +{ + if (!canRefreshSchemaSnapshot()) { + if (PHP_SAPI !== 'cli') { + http_response_code(403); + } + + snapshotOutput('Forbidden: run refresh_complete_schema.php from CLI, localhost, or while logged in as an Administrator.'); + return 1; + } + + try { + $pdo = db(); + $tableOrder = []; + $snapshotSql = buildSnapshotContent($pdo, $tableOrder); + $targetFile = __DIR__ . '/complete_schema.sql'; + $bytesWritten = file_put_contents($targetFile, $snapshotSql); + + if ($bytesWritten === false) { + throw new RuntimeException('Unable to write complete_schema.sql'); + } + + snapshotOutput('OK: refreshed complete_schema.sql'); + snapshotOutput('Tables: ' . count($tableOrder)); + snapshotOutput('Path: complete_schema.sql'); + return 0; + } catch (Throwable $throwable) { + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + + snapshotOutput('ERROR: ' . $throwable->getMessage()); + return 1; + } +} + +exit(runSchemaSnapshotRefresh());