From 675156eee177bc6c00f30de59c31a26ff94e9f62 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 18 Feb 2026 10:32:06 +0000 Subject: [PATCH] add installation module --- .gitignore | 3 + assets/css/custom.css | 14 +- check_nesting.php | 48 ++++ cron_backup.php | 37 +++ db/BackupService.php | 102 ++++++++ debug_395.txt | 31 +++ index.php | 528 ++++++++++++++++++++++++++++------------- installation/index.php | 258 ++++++++++++++++++++ post_debug.log | 8 + 9 files changed, 868 insertions(+), 161 deletions(-) create mode 100644 check_nesting.php create mode 100644 cron_backup.php create mode 100644 db/BackupService.php create mode 100644 debug_395.txt create mode 100644 installation/index.php diff --git a/.gitignore b/.gitignore index e427ff3..7b529ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules/ */node_modules/ */build/ +sessions/ +backups/ +*.lock diff --git a/assets/css/custom.css b/assets/css/custom.css index d204d85..79a56b5 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -79,15 +79,25 @@ body { color: #94a3b8 !important; } -.nav-section-title i.bi-chevron-down { +.nav-section-title i.chevron { transition: transform 0.2s; font-size: 0.6rem; } -.nav-section-title.collapsed i.bi-chevron-down { +.nav-section-title.collapsed i.chevron { transform: rotate(-90deg); } +.nav-section-title .group-icon { + width: 18px; + margin-right: 10px; +} + +[dir="rtl"] .nav-section-title .group-icon { + margin-right: 0; + margin-left: 10px; +} + /* POS Styles */ .pos-container { display: flex; diff --git a/check_nesting.php b/check_nesting.php new file mode 100644 index 0000000..6eaee8a --- /dev/null +++ b/check_nesting.php @@ -0,0 +1,48 @@ + $line) { + $line_num = $idx + 1; + + // Simple check for PHP tags + if (strpos($line, '') !== false) $in_php = false; + + // Alternative syntax checks + if (preg_match('/\bif\b\s*\(.*\)\s*:/', $line)) { + $stack[] = ['type' => 'if', 'line' => $line_num]; + } elseif (preg_match('/\belseif\b\s*\(.*\)\s*:/', $line)) { + // elseif is part of the current if block, so it doesn't change nesting level + } elseif (preg_match('/\belse\b\s*:/', $line)) { + // else is part of the current if block, so it doesn't change nesting level + } elseif (preg_match('/foreach\s*\(.*\)\s*:/', $line)) { + $stack[] = ['type' => 'foreach', 'line' => $line_num]; + } + if (strpos($line, 'endif;') !== false) { + if (empty($stack)) { + echo "Unexpected endif; at line $line_num\n"; + } else { + $last = array_pop($stack); + if ($last['type'] !== 'if') { + echo "Mismatched endif; at line $line_num (expected endforeach; for {$last['type']} at line {$last['line']})\n"; + } + } + } + if (strpos($line, 'endforeach;') !== false) { + if (empty($stack)) { + echo "Unexpected endforeach; at line $line_num\n"; + } else { + $last = array_pop($stack); + if ($last['type'] !== 'foreach') { + echo "Mismatched endforeach; at line $line_num (expected endif; for {$last['type']} at line {$last['line']})\n"; + } + } + } +} + +foreach ($stack as $unclosed) { + echo "Unclosed {$unclosed['type']} starting at line {$unclosed['line']}\n"; +} diff --git a/cron_backup.php b/cron_backup.php new file mode 100644 index 0000000..c579575 --- /dev/null +++ b/cron_backup.php @@ -0,0 +1,37 @@ +prepare("SELECT `key`, `value` FROM settings WHERE `key` IN ('backup_auto_enabled', 'backup_limit', 'backup_time')"); +$stmt->execute(); +$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + +$enabled = $settings['backup_auto_enabled'] ?? '0'; + +if ($enabled === '1') { + $scheduledTime = $settings['backup_time'] ?? '00:00'; + $currentTime = date('H:i'); + + if ($currentTime !== $scheduledTime) { + die("[" . date('Y-m-d H:i:s') . "] Not the scheduled time ($scheduledTime). Current time: $currentTime\n"); + } + + $limit = $settings['backup_limit'] ?? 5; + + echo "[" . date('Y-m-d H:i:s') . "] Starting automated backup...\n"; + $res = BackupService::createBackup(); + + if ($res['success']) { + echo "[" . date('Y-m-d H:i:s') . "] Backup created successfully: " . $res['file'] . "\n"; + } else { + echo "[" . date('Y-m-d H:i:s') . "] Error creating backup: " . $res['error'] . "\n"; + } +} else { + echo "[" . date('Y-m-d H:i:s') . "] Automated backup is disabled in settings.\n"; +} diff --git a/db/BackupService.php b/db/BackupService.php new file mode 100644 index 0000000..92cae4e --- /dev/null +++ b/db/BackupService.php @@ -0,0 +1,102 @@ + %s', + escapeshellarg(DB_HOST), + escapeshellarg(DB_USER), + escapeshellarg(DB_PASS), + escapeshellarg(DB_NAME), + escapeshellarg($filePath) + ); + + exec($command, $output, $returnVar); + + if ($returnVar === 0) { + // Get limit from settings + $limit = 5; + try { + $stmt = db()->prepare("SELECT `value` FROM settings WHERE `key` = 'backup_limit'"); + $stmt->execute(); + $val = $stmt->fetchColumn(); + if ($val) $limit = (int)$val; + } catch (Exception $e) {} + + self::rotateBackups($limit); + return ['success' => true, 'file' => $filename]; + } + + return ['success' => false, 'error' => 'Failed to create backup.']; + } + + public static function restoreBackup($filename) { + $filePath = self::$backupDir . basename($filename); + if (!file_exists($filePath)) { + return ['success' => false, 'error' => 'Backup file not found.']; + } + + $command = sprintf( + 'mysql -h %s -u %s -p%s %s < %s', + escapeshellarg(DB_HOST), + escapeshellarg(DB_USER), + escapeshellarg(DB_PASS), + escapeshellarg(DB_NAME), + escapeshellarg($filePath) + ); + + exec($command, $output, $returnVar); + + if ($returnVar === 0) { + return ['success' => true]; + } + + return ['success' => false, 'error' => 'Failed to restore backup.']; + } + + public static function rotateBackups($limit = 5) { + $files = glob(self::$backupDir . 'backup_*.sql'); + if (count($files) <= $limit) { + return; + } + + // Sort by modification time (oldest first) + usort($files, function($a, $b) { + return filemtime($a) - filemtime($b); + }); + + $toDelete = count($files) - $limit; + for ($i = 0; $i < $toDelete; $i++) { + unlink($files[$i]); + } + } + + public static function getBackups() { + if (!is_dir(self::$backupDir)) return []; + $files = glob(self::$backupDir . 'backup_*.sql'); + usort($files, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + $result = []; + foreach ($files as $file) { + $result[] = [ + 'name' => basename($file), + 'size' => round(filesize($file) / 1024, 2) . ' KB', + 'date' => date('Y-m-d H:i:s', filemtime($file)) + ]; + } + return $result; + } +} diff --git a/debug_395.txt b/debug_395.txt new file mode 100644 index 0000000..1f2fd7f --- /dev/null +++ b/debug_395.txt @@ -0,0 +1,31 @@ + + // --- User & Role Groups Handlers --- + if (isset($_POST['add_role_group'])) { + $name = $_POST['name'] ?? ''; + $permissions = isset($_POST['permissions']) ? json_encode($_POST['permissions']) : '[]'; + if ($name) { + try { + $stmt = db()->prepare("INSERT INTO role_groups (name, permissions) VALUES (?, ?)"); + $stmt->execute([$name, $permissions]); + $message = "Role Group added successfully!"; + } catch (PDOException $e) { + $message = "Error adding role group: " . $e->getMessage(); + } + } + } + + if (isset($_POST['add_user'])) { + $username = $_POST['username'] ?? ''; + $password = $_POST['password'] ?? ''; + $email = $_POST['email'] ?? ''; + $group_id = (int)($_POST['group_id'] ?? 0) ?: null; + if ($username && $password) { + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + $stmt = db()->prepare("INSERT INTO users (username, password, email, group_id) VALUES (?, ?, ?, ?)"); + try { + $stmt->execute([$username, $hashed_password, $email, $group_id]); + $message = "User added successfully!"; + } catch (PDOException $e) { + if ($e->getCode() == '23000') { + $message = "Error: Username already exists."; + } else { diff --git a/index.php b/index.php index 73aa12c..40ecc7c 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,7 @@ prepare("INSERT INTO users (username, password, email, group_id) VALUES (?, ?, ?, ?)"); + $stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?)"); try { - $stmt->execute([$username, $hashed_password, $email, $group_id]); + $stmt->execute([$username, $hashed_password, $email, $phone, $group_id]); $message = "User added successfully!"; } catch (PDOException $e) { if ($e->getCode() == '23000') { @@ -435,11 +439,12 @@ if (isset($_POST['add_hr_department'])) { $id = (int)$_POST['id']; $username = $_POST['username'] ?? ''; $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; $group_id = (int)($_POST['group_id'] ?? 0) ?: null; $status = $_POST['status'] ?? 'active'; if ($id && $username) { - $stmt = db()->prepare("UPDATE users SET username = ?, email = ?, group_id = ?, status = ? WHERE id = ?"); - $stmt->execute([$username, $email, $group_id, $status, $id]); + $stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ? WHERE id = ?"); + $stmt->execute([$username, $email, $phone, $group_id, $status, $id]); if (!empty($_POST['password'])) { $hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT); @@ -458,6 +463,77 @@ if (isset($_POST['add_hr_department'])) { } } + if (isset($_POST['update_profile'])) { + $id = $_SESSION['user_id']; + $username = $_POST['username'] ?? ''; + $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; + + if ($id && $username) { + $stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ? WHERE id = ?"); + $stmt->execute([$username, $email, $phone, $id]); + $_SESSION['username'] = $username; + + if (!empty($_POST['password'])) { + $hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT); + $stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?"); + $stmt->execute([$hashed_password, $id]); + } + + if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === 0) { + $ext = pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION); + $filename = 'uploads/profile_' . $id . '_' . time() . '.' . $ext; + if (!is_dir('uploads')) mkdir('uploads', 0777, true); + if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $filename)) { + $stmt = db()->prepare("UPDATE users SET profile_pic = ? WHERE id = ?"); + $stmt->execute([$filename, $id]); + $_SESSION['profile_pic'] = $filename; + } + } + $message = "Profile updated successfully!"; + } + } + + // --- Backup Handlers --- + if (isset($_POST['create_backup'])) { + if (can('users_view')) { // Admin check + $res = BackupService::createBackup(); + $message = $res['success'] ? "Backup created: " . $res['file'] : "Error: " . $res['error']; + } + } + + if (isset($_POST['restore_backup'])) { + if (can('users_view')) { + $filename = $_POST['filename'] ?? ''; + $res = BackupService::restoreBackup($filename); + $message = $res['success'] ? "Database restored successfully from $filename!" : "Error: " . $res['error']; + } + } + + if (isset($_POST['delete_backup'])) { + if (can('users_view')) { + $filename = basename($_POST['filename'] ?? ''); + if (unlink(__DIR__ . '/backups/' . $filename)) { + $message = "Backup deleted successfully."; + } else { + $message = "Error deleting backup."; + } + } + } + + if (isset($_POST['save_backup_settings'])) { + if (can('users_view')) { + $limit = (int)($_POST['backup_limit'] ?? 5); + $auto = $_POST['backup_auto_enabled'] ?? '0'; + $time = $_POST['backup_time'] ?? '00:00'; + + $db = db(); + $stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES ('backup_limit', ?), ('backup_auto_enabled', ?), ('backup_time', ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)"); + $stmt->execute([$limit, $auto, $time]); + $message = "Backup settings saved successfully!"; + } + } + // Routing & Data Fetching $page = $_GET['page'] ?? 'dashboard'; @@ -494,6 +570,7 @@ $page_permissions = [ 'hr_payroll' => 'hr_view', 'role_groups' => 'users_view', 'users' => 'users_view', + 'backups' => 'users_view', ]; if (isset($page_permissions[$page]) && !can($page_permissions[$page])) { @@ -501,7 +578,28 @@ if (isset($page_permissions[$page]) && !can($page_permissions[$page])) { $message = "Access Denied: You don't have permission to view that module."; } -$data = []; +$data = [ + 'payment_methods' => [], + 'role_groups' => [], + 'users' => [], + 'expiry_items' => [], + 'low_stock_items' => [], + 'items' => [], + 'cash_transactions' => [], + 'monthly_sales' => [], + 'yearly_sales' => [], + 'opening_balance' => 0, + 'stats' => [ + 'expired_items' => 0, + 'near_expiry_items' => 0, + 'low_stock_items_count' => 0, + 'total_sales' => 0, + 'total_received' => 0, + 'total_receivable' => 0, + 'total_purchases' => 0, + ], + 'settings' => [], +]; if ($page === 'export') { $type = $_GET['type'] ?? 'sales'; @@ -832,6 +930,12 @@ switch ($page) { $data['users'] = db()->query("SELECT u.*, g.name as group_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id ORDER BY u.username ASC")->fetchAll(); $data['role_groups'] = db()->query("SELECT id, name FROM role_groups ORDER BY name ASC")->fetchAll(); break; + case 'backups': + $data['backups'] = BackupService::getBackups(); + $stmt = db()->prepare("SELECT * FROM settings WHERE `key` IN ('backup_limit', 'backup_auto_enabled', 'backup_time')"); + $stmt->execute(); + $data['backup_settings'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + break; case 'accounting': $data['journal_entries'] = db()->query("SELECT je.*, (SELECT SUM(debit) FROM acc_ledger WHERE journal_entry_id = je.id) as total_debit @@ -1050,6 +1154,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + @@ -1075,179 +1180,122 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - Dashboard + Dashboard + + + +
+ + Products + + + Categories + + + Units + +
+ + + + + +
+ + + Customers + + + + + Suppliers + + +
+ + - + +
+
+
+
+
Backup Settings
+
+
+
+
+ + +
+
+ + +
+
+
+ > + +
+ Requires a cron job running cron_backup.php every minute to respect the scheduled time. +
+ +
+
+
+ +
+
+
+ +
+
Manual Backup
+

Create a database backup immediately.

+
+ +
+
+
+
+ +
+
+
+
Available Backups
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FilenameSizeDateActions
No backups found.
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
@@ -5258,7 +5457,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; User Info Access Level - Email + Contact Status Actions @@ -5286,7 +5485,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - + +
+
+ Active @@ -5334,6 +5536,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
+
+ + +
+
+ + +
diff --git a/installation/index.php b/installation/index.php new file mode 100644 index 0000000..60d3f68 --- /dev/null +++ b/installation/index.php @@ -0,0 +1,258 @@ + [ + 'name' => 'PHP Version (>= 8.0)', + 'status' => version_compare(PHP_VERSION, '8.0.0', '>='), + 'current' => PHP_VERSION + ], + 'pdo_mysql' => [ + 'name' => 'PDO MySQL Extension', + 'status' => extension_loaded('pdo_mysql'), + 'current' => extension_loaded('pdo_mysql') ? 'Loaded' : 'Missing' + ], + 'config_writable' => [ + 'name' => 'db/config.php Writable', + 'status' => is_writable(__DIR__ . '/../db/config.php'), + 'current' => is_writable(__DIR__ . '/../db/config.php') ? 'Yes' : 'No' + ], + 'root_writable' => [ + 'name' => 'Root Directory Writable', + 'status' => is_writable(__DIR__ . '/..'), + 'current' => is_writable(__DIR__ . '/..') ? 'Yes' : 'No' + ] + ]; + $allOk = true; + foreach ($requirements as $req) { + if (!$req['status']) $allOk = false; + } +} + +// Step 2: Database +if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') { + $host = $_POST['db_host'] ?? ''; + $name = $_POST['db_name'] ?? ''; + $user = $_POST['db_user'] ?? ''; + $pass = $_POST['db_pass'] ?? ''; + + try { + $pdo = new PDO("mysql:host=$host;dbname=$name", $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + + // Save to config.php + $configContent = " PDO::ERRMODE_EXCEPTION,\n PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n ]);\n }\n return \$pdo;\n}\n"; + file_put_contents(__DIR__ . '/../db/config.php', $configContent); + + header("Location: index.php?step=3"); + exit; + } catch (PDOException $e) { + $error = "Database Connection Failed: " . $e->getMessage(); + } +} + +// Step 3: Super Admin +if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') { + require_once __DIR__ . '/../db/config.php'; + $adminUser = $_POST['admin_user'] ?? ''; + $adminPass = $_POST['admin_pass'] ?? ''; + $adminEmail = $_POST['admin_email'] ?? ''; + + try { + $pdo = db(); + + // Create tables if they don't exist + $pdo->exec("CREATE TABLE IF NOT EXISTS role_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL, + permissions TEXT + )"); + + $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 + )"); + + // Insert Default Admin Role if missing + $stmt = $pdo->prepare("SELECT id FROM role_groups WHERE name = 'Administrator'"); + $stmt->execute(); + $role = $stmt->fetch(); + if (!$role) { + $pdo->exec("INSERT INTO role_groups (name, permissions) VALUES ('Administrator', 'all')"); + $roleId = $pdo->lastInsertId(); + } else { + $roleId = $role['id']; + } + + // Insert Admin User + $hashedPass = password_hash($adminPass, PASSWORD_DEFAULT); + $stmt = $pdo->prepare("INSERT INTO users (username, password, email, group_id, status) VALUES (?, ?, ?, ?, 'active')"); + $stmt->execute([$adminUser, $hashedPass, $adminEmail, $roleId]); + + header("Location: index.php?step=4"); + exit; + } catch (Exception $e) { + $error = "Setup Failed: " . $e->getMessage(); + } +} + +// Step 4: Finish +if ($step === 4) { + file_put_contents($lockFile, date('Y-m-d H:i:s')); +} + +?> + + + + + + Installation - Step <?= $step ?> + + + + + +
+
+
+

System Installation

+

Configure your application in minutes

+
+
+
+
1 ? '' : '1' ?>
+
2 ? '' : '2' ?>
+
3 ? '' : '3' ?>
+
4
+
+ + +
+ +
+
+ + + +
Step 1: Check Requirements
+
+ +
+
+
+ Current: +
+ + Passed + + Failed + +
+ +
+ +
+ +
Please fix the issues above to continue.
+
+ +
+ + + +
Step 2: Database Configuration
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
Step 3: Super Admin Setup
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+

Ready to Launch!

+

Installation completed successfully. Your system is now secure and ready to use.

+
+ + For security, the installed.lock file has been created. +
+ +
+ +
+
+
+ + diff --git a/post_debug.log b/post_debug.log index e69de29..0da4d7f 100644 --- a/post_debug.log +++ b/post_debug.log @@ -0,0 +1,8 @@ +2026-02-18 09:15:41 - POST: {"username":"admin","password":"admin","login":""} +2026-02-18 09:30:37 - POST: {"username":"moosa","email":"aalabry@gmail.com","password":"123456","group_id":"1","add_user":""} +2026-02-18 09:32:14 - POST: {"name":"Cashier","permissions":["pos_view","pos_add","items_view","items_add"],"add_role_group":""} +2026-02-18 09:33:29 - POST: {"name":"Admin","permissions":["pos_view","pos_add","pos_edit","quotations_view","quotations_add","quotations_edit","customers_view","customers_add","customers_edit","suppliers_view","suppliers_add","suppliers_edit","sales_view","sales_add","sales_edit","purchases_view","purchases_add","purchases_edit","hr_view","hr_add","hr_edit","hr_delete","users_view"],"add_role_group":""} +2026-02-18 09:34:03 - POST: {"name":"Accountant","permissions":["accounting_view","accounting_add","accounting_edit"],"add_role_group":""} +2026-02-18 10:10:35 - POST: {"name":"Accountant","permissions":["accounting_view","accounting_add","accounting_edit"],"add_role_group":""} +2026-02-18 10:10:56 - POST: {"id":"7","delete_role_group":""} +2026-02-18 10:12:05 - POST: {"create_backup":""}