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 + + +
+ + + + + + +