update schema
This commit is contained in:
parent
0e49a0bd6f
commit
3a757163ae
891
complete_schema.sql
Normal file
891
complete_schema.sql
Normal file
@ -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 */;
|
||||
5
cookies_debug.txt
Normal file
5
cookies_debug.txt
Normal file
@ -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
|
||||
5
cookies_verify.txt
Normal file
5
cookies_verify.txt
Normal file
@ -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
|
||||
313
db/migrations/20260502_full_schema_sync.php
Normal file
313
db/migrations/20260502_full_schema_sync.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!function_exists('full_schema_sync_20260502_run')) {
|
||||
function full_schema_sync_20260502_run(): void
|
||||
{
|
||||
$pdo = db();
|
||||
|
||||
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/../schema.sql');
|
||||
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_create_outlets_table.sql');
|
||||
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_multi_outlet_schema.sql');
|
||||
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_user_outlets_table.sql');
|
||||
|
||||
full_schema_sync_20260502_ensure_columns($pdo);
|
||||
full_schema_sync_20260502_seed_defaults($pdo);
|
||||
}
|
||||
|
||||
function full_schema_sync_20260502_apply_create_tables_from_file(PDO $pdo, string $filePath): void
|
||||
{
|
||||
if (!is_file($filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = file_get_contents($filePath);
|
||||
if ($sql === false) {
|
||||
throw new RuntimeException('Unable to read schema source: ' . basename($filePath));
|
||||
}
|
||||
|
||||
foreach (full_schema_sync_20260502_extract_create_table_statements($sql) as $statement) {
|
||||
$normalized = full_schema_sync_20260502_normalize_create_table_statement($statement);
|
||||
full_schema_sync_20260502_exec($pdo, $normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function full_schema_sync_20260502_extract_create_table_statements(string $sql): array
|
||||
{
|
||||
if (!preg_match_all('/CREATE\s+TABLE\b.*?;/is', $sql, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $matches[0] ?? [];
|
||||
}
|
||||
|
||||
function full_schema_sync_20260502_normalize_create_table_statement(string $statement): string
|
||||
{
|
||||
$statement = trim($statement);
|
||||
$statement = preg_replace('/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+/i', 'CREATE TABLE IF NOT EXISTS ', $statement, 1) ?? $statement;
|
||||
|
||||
$lines = preg_split('/\R/', $statement) ?: [];
|
||||
$kept = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*CONSTRAINT\b/i', $line)) {
|
||||
continue;
|
||||
}
|
||||
$kept[] = $line;
|
||||
}
|
||||
|
||||
$statement = implode("\n", $kept);
|
||||
$statement = preg_replace('/,\s*\)(?=\s*(ENGINE|DEFAULT|COMMENT|CHARSET|COLLATE|;))/is', "\n)", $statement) ?? $statement;
|
||||
|
||||
return rtrim($statement, "\n\r\t ;") . ';';
|
||||
}
|
||||
|
||||
function full_schema_sync_20260502_ensure_columns(PDO $pdo): void
|
||||
{
|
||||
$columns = [
|
||||
'role_groups' => [
|
||||
'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;
|
||||
@ -1,21 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class DatabaseInstaller {
|
||||
public static function isInstalled() {
|
||||
public static function isInstalled(): bool {
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
$stmt = $db->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]);
|
||||
}
|
||||
}
|
||||
|
||||
140
index.php
140
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'])) {
|
||||
<form method="POST">
|
||||
<div class="mb-3 text-start">
|
||||
<label class="form-label small fw-semibold"><?= __('username') ?></label>
|
||||
<input type="text" name="username" class="form-control" placeholder="admin" required autofocus>
|
||||
<input type="text" name="username" class="form-control" placeholder="<?= $lang === 'ar' ? 'اسم المستخدم' : 'Username' ?>" required autofocus>
|
||||
</div>
|
||||
<div class="mb-4 text-start">
|
||||
<label class="form-label small fw-semibold"><?= __('password') ?></label>
|
||||
<input type="password" name="password" class="form-control" placeholder="••••••••" required>
|
||||
<input type="password" name="password" class="form-control" placeholder="<?= $lang === 'ar' ? 'كلمة المرور' : 'Password' ?>" required>
|
||||
</div>
|
||||
<button type="submit" name="login" class="btn btn-primary w-100 mb-3"><?= __('sign_in') ?></button>
|
||||
<div class="text-muted small"><?= $lang === 'ar' ? 'المستخدم الافتراضي: admin / admin' : 'Default user: admin / admin' ?></div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"}
|
||||
|
||||
308
refresh_complete_schema.php
Normal file
308
refresh_complete_schema.php
Normal file
@ -0,0 +1,308 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('X-Robots-Tag: noindex, nofollow');
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
|
||||
function snapshotOutput(string $message = ''): void
|
||||
{
|
||||
echo $message . PHP_EOL;
|
||||
}
|
||||
|
||||
function configureSnapshotSession(): void
|
||||
{
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionsDir = __DIR__ . '/sessions';
|
||||
if (!is_dir($sessionsDir)) {
|
||||
@mkdir($sessionsDir, 0777, true);
|
||||
}
|
||||
|
||||
if (is_writable($sessionsDir)) {
|
||||
session_save_path($sessionsDir);
|
||||
}
|
||||
|
||||
if (
|
||||
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
|
||||
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|
||||
) {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 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());
|
||||
Loading…
x
Reference in New Issue
Block a user