One Hat Cyber Team
Your IP :
216.73.216.55
Server IP :
5.189.175.239
Server :
Linux panel.gemx-ai.com 5.14.0-570.19.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Jun 4 04:00:24 EDT 2025 x86_64
Server Software :
LiteSpeed
PHP Version :
8.2.28
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
home
/
farmersapp
/
loans.farmersapp.store
/
includes
/
View File Name :
functions.php
<?php /** * Common Utility Functions * Farmers Loan Management System */ // ============================================= // FORMATTING FUNCTIONS // ============================================= /** * Format currency */ function formatCurrency($amount, $currency = null) { if ($currency === null) { $currency = DEFAULT_CURRENCY; } return number_format($amount, 2) . ' ' . $currency; } /** * Format date for display */ function formatDate($date, $format = null) { if (empty($date) || $date === '0000-00-00') { return 'N/A'; } if ($format === null) { $format = DATE_FORMAT_DISPLAY; } $timestamp = strtotime($date); return date($format, $timestamp); } /** * Format date time for display */ function formatDateTime($datetime, $format = null) { if (empty($datetime) || $datetime === '0000-00-00 00:00:00') { return 'N/A'; } if ($format === null) { $format = DATETIME_FORMAT_DISPLAY; } $timestamp = strtotime($datetime); return date($format, $timestamp); } /** * Format phone number */ function formatPhone($phone) { if (empty($phone)) { return 'N/A'; } // Remove all non-digit characters $clean = preg_replace('/[^0-9]/', '', $phone); // Format based on length if (strlen($clean) === 9) { return preg_replace('/(\d{3})(\d{3})(\d{3})/', '$1 $2 $3', $clean); } elseif (strlen($clean) === 12) { return preg_replace('/(\d{3})(\d{3})(\d{3})(\d{3})/', '+$1 $2 $3 $4', $clean); } return $phone; } /** * Truncate text with ellipsis */ function truncateText($text, $length = 100, $ellipsis = '...') { if (strlen($text) <= $length) { return $text; } $truncated = substr($text, 0, $length); $lastSpace = strrpos($truncated, ' '); if ($lastSpace !== false) { $truncated = substr($truncated, 0, $lastSpace); } return $truncated . $ellipsis; } /** * Generate slug from text */ function generateSlug($text) { $slug = strtolower($text); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); $slug = trim($slug, '-'); return $slug; } // ============================================= // VALIDATION FUNCTIONS // ============================================= /** * Validate required fields */ function validateRequired($data, $fields) { $errors = []; foreach ($fields as $field) { if (empty($data[$field])) { $errors[$field] = "The {$field} field is required"; } } return [ 'valid' => empty($errors), 'errors' => $errors ]; } /** * Validate email */ function validateEmail($email) { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } /** * Validate phone number */ function validatePhoneNumber($phone) { // Remove all non-digit characters $clean = preg_replace('/[^0-9]/', '', $phone); // Check if it's a valid length return strlen($clean) >= 10 && strlen($clean) <= 15; } /** /** * Validate date */ function validateDateString($date, $format = 'Y-m-d') { $d = DateTime::createFromFormat($format, $date); return $d && $d->format($format) === $date; } /** * Validate numeric range */ function validateNumericRange($value, $min = null, $max = null) { if (!is_numeric($value)) { return false; } if ($min !== null && $value < $min) { return false; } if ($max !== null && $value > $max) { return false; } return true; } /** * Validate file upload */ function validateFileUpload($file, $allowedTypes = null, $maxSize = null) { if ($allowedTypes === null) { $allowedTypes = explode(',', ALLOWED_FILE_TYPES); } if ($maxSize === null) { $maxSize = MAX_UPLOAD_SIZE; } $errors = []; // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = getUploadErrorMessage($file['error']); return ['valid' => false, 'errors' => $errors]; } // Check file size if ($file['size'] > $maxSize) { $errors[] = "File size exceeds maximum allowed size of " . formatFileSize($maxSize); } // Check file type $fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($fileExt, $allowedTypes)) { $errors[] = "File type not allowed. Allowed types: " . implode(', ', $allowedTypes); } return [ 'valid' => empty($errors), 'errors' => $errors, 'extension' => $fileExt ]; } /** * Get upload error message */ function getUploadErrorMessage($errorCode) { switch ($errorCode) { case UPLOAD_ERR_INI_SIZE: return 'The uploaded file exceeds the upload_max_filesize directive in php.ini'; case UPLOAD_ERR_FORM_SIZE: return 'The uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form'; case UPLOAD_ERR_PARTIAL: return 'The uploaded file was only partially uploaded'; case UPLOAD_ERR_NO_FILE: return 'No file was uploaded'; case UPLOAD_ERR_NO_TMP_DIR: return 'Missing a temporary folder'; case UPLOAD_ERR_CANT_WRITE: return 'Failed to write file to disk'; case UPLOAD_ERR_EXTENSION: return 'A PHP extension stopped the file upload'; default: return 'Unknown upload error'; } } // ============================================= // FILE AND DIRECTORY FUNCTIONS // ============================================= /** * Format file size */ function formatFileSize($bytes, $decimals = 2) { $size = ['B', 'KB', 'MB', 'GB', 'TB']; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . $size[$factor]; } /** * Create directory if it doesn't exist */ function createDirectory($path, $permissions = 0755) { if (!file_exists($path)) { return mkdir($path, $permissions, true); } return true; } /** * Generate unique filename */ function generateUniqueFilename($originalName, $directory = null) { $extension = pathinfo($originalName, PATHINFO_EXTENSION); $basename = pathinfo($originalName, PATHINFO_FILENAME); // Generate unique name $uniqueName = uniqid() . '_' . time() . '_' . preg_replace('/[^a-zA-Z0-9]/', '_', $basename); if ($extension) { $uniqueName .= '.' . $extension; } // If directory is provided, check for collisions if ($directory && file_exists($directory . '/' . $uniqueName)) { return generateUniqueFilename($originalName, $directory); } return $uniqueName; } /** * Save uploaded file */ function saveUploadedFile($file, $destinationDir, $allowedTypes = null) { // Validate upload $validation = validateFileUpload($file, $allowedTypes); if (!$validation['valid']) { return [ 'success' => false, 'errors' => $validation['errors'] ]; } // Create directory if it doesn't exist if (!createDirectory($destinationDir)) { return [ 'success' => false, 'errors' => ['Failed to create destination directory'] ]; } // Generate unique filename $filename = generateUniqueFilename($file['name'], $destinationDir); $destination = $destinationDir . '/' . $filename; // Move uploaded file if (move_uploaded_file($file['tmp_name'], $destination)) { return [ 'success' => true, 'filename' => $filename, 'path' => $destination, 'size' => $file['size'], 'type' => $file['type'] ]; } return [ 'success' => false, 'errors' => ['Failed to save uploaded file'] ]; } /** * Delete file */ function deleteFile($filepath) { if (file_exists($filepath) && is_file($filepath)) { return unlink($filepath); } return false; } // ============================================= // DATABASE HELPER FUNCTIONS // ============================================= /** * Get loan statistics */ function getLoanStatistics($branchId = null, $status = null) { $db = Database::getInstance(); $conditions = []; if ($branchId !== null) { $conditions['branch_id'] = $branchId; } if ($status !== null) { $conditions['status'] = $status; } $stats = []; // Total loans $stats['total_loans'] = $db->count('loans', $conditions); // Total principal amount $stats['total_principal'] = $db->sum('loans', 'principal_amount', $conditions); // Total interest $stats['total_interest'] = $db->sum('loans', 'total_interest', $conditions); // Total amount $stats['total_amount'] = $db->sum('loans', 'total_amount', $conditions); // Average loan amount $stats['average_loan'] = $stats['total_loans'] > 0 ? $stats['total_principal'] / $stats['total_loans'] : 0; return $stats; } /** * Get borrower statistics */ function getBorrowerStatistics($branchId = null) { $db = Database::getInstance(); $conditions = ['status' => 'active']; if ($branchId !== null) { $conditions['branch_id'] = $branchId; } $stats = []; // Total active borrowers $stats['total_borrowers'] = $db->count('borrowers', $conditions); // Borrowers by gender $genderStats = $db->raw(" SELECT gender, COUNT(*) as count FROM borrowers WHERE status = 'active' " . ($branchId ? " AND branch_id = $branchId" : "") . " GROUP BY gender "); $stats['by_gender'] = $genderStats; // Borrowers by risk category $riskStats = $db->raw(" SELECT risk_category, COUNT(*) as count FROM borrowers WHERE status = 'active' " . ($branchId ? " AND branch_id = $branchId" : "") . " GROUP BY risk_category "); $stats['by_risk'] = $riskStats; return $stats; } /** * Get repayment statistics */ function getRepaymentStatistics($branchId = null, $startDate = null, $endDate = null) { $db = Database::getInstance(); $conditions = []; if ($branchId !== null) { $conditions['branch_id'] = $branchId; } if ($startDate !== null) { $conditions['payment_date >='] = $startDate; } if ($endDate !== null) { $conditions['payment_date <='] = $endDate; } $stats = []; // Total repayments $stats['total_repayments'] = $db->count('repayments', $conditions); // Total amount collected $stats['total_collected'] = $db->sum('repayments', 'amount', $conditions); // Average repayment amount $stats['average_repayment'] = $stats['total_repayments'] > 0 ? $stats['total_collected'] / $stats['total_repayments'] : 0; return $stats; } /** * Get overdue loans */ function getOverdueLoans($branchId = null) { $db = Database::getInstance(); $sql = "SELECT l.*, b.first_name, b.last_name, b.phone, DATEDIFF(CURDATE(), li.due_date) as days_overdue, li.amount_due - li.paid_amount as amount_due FROM loans l INNER JOIN borrowers b ON l.borrower_id = b.id INNER JOIN loan_installments li ON l.id = li.loan_id WHERE l.status = 'active' AND li.status = 'overdue' AND li.due_date < CURDATE()"; if ($branchId !== null) { $sql .= " AND l.branch_id = ?"; return $db->prepare($sql)->resultArray([$branchId]); } return $db->prepare($sql)->resultArray(); } /** * Get upcoming repayments */ function getUpcomingRepayments($days = 7, $branchId = null) { $db = Database::getInstance(); $sql = "SELECT l.*, b.first_name, b.last_name, b.phone, li.due_date, li.amount_due, li.paid_amount, DATEDIFF(li.due_date, CURDATE()) as days_until_due FROM loans l INNER JOIN borrowers b ON l.borrower_id = b.id INNER JOIN loan_installments li ON l.id = li.loan_id WHERE l.status = 'active' AND li.status = 'pending' AND li.due_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)"; $params = [$days]; if ($branchId !== null) { $sql .= " AND l.branch_id = ?"; $params[] = $branchId; } $sql .= " ORDER BY li.due_date ASC"; return $db->prepare($sql)->resultArray($params); } // ============================================= // NOTIFICATION FUNCTIONS // ============================================= /** * Create notification */ function createNotification($userId, $title, $message, $type = 'info', $actionUrl = null) { $db = Database::getInstance(); return $db->insert('notifications', [ 'user_id' => $userId, 'title' => $title, 'message' => $message, 'type' => $type, 'action_url' => $actionUrl, 'is_read' => 0 ]); } /** * Get user notifications */ function getUserNotifications($userId, $unreadOnly = false, $limit = 20) { $db = Database::getInstance(); $conditions = ['user_id' => $userId]; if ($unreadOnly) { $conditions['is_read'] = 0; } return $db->select('notifications', $conditions, '*', 'created_at DESC', $limit); } /** * Mark notification as read */ function markNotificationAsRead($notificationId, $userId = null) { $db = Database::getInstance(); $conditions = ['id' => $notificationId]; if ($userId !== null) { $conditions['user_id'] = $userId; } return $db->update('notifications', ['is_read' => 1], $conditions); } /** * Mark all notifications as read */ function markAllNotificationsAsRead($userId) { $db = Database::getInstance(); return $db->update('notifications', ['is_read' => 1], ['user_id' => $userId, 'is_read' => 0] ); } /** * Get unread notification count */ function getUnreadNotificationCount($userId) { $db = Database::getInstance(); return $db->count('notifications', ['user_id' => $userId, 'is_read' => 0]); } // ============================================= // LOGGING FUNCTIONS // ============================================= /** * Log system action */ function logAction($action, $description, $entityId = null, $entityType = null) { $db = Database::getInstance(); return $db->insert('audit_logs', [ 'user_id' => $_SESSION['user_id'] ?? null, 'action' => $action, 'description' => $description, 'entity_type' => $entityType, 'entity_id' => $entityId, 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown' ]); } /** * Get audit logs */ function getAuditLogs($filters = [], $limit = 50) { $db = Database::getInstance(); $query = table('audit_logs al') ->select(['al.*', 'u.username', 'u.first_name', 'u.last_name']) ->leftJoin('users u', 'al.user_id', '=', 'u.id'); // Apply filters foreach ($filters as $key => $value) { if ($value !== null) { $query->where("al.$key", '=', $value); } } return $query->orderBy('al.created_at', 'DESC')->limit($limit)->get(); } // ============================================= // SETTINGS FUNCTIONS // ============================================= /** * Get system setting */ function getSetting($key, $default = null) { $db = Database::getInstance(); $setting = $db->selectOne('settings', ['setting_key' => $key]); if ($setting) { return $setting['setting_value']; } return $default; } /** * Update system setting */ function updateSetting($key, $value) { $db = Database::getInstance(); if ($db->exists('settings', ['setting_key' => $key])) { return $db->update('settings', ['setting_value' => $value], ['setting_key' => $key] ); } else { return $db->insert('settings', [ 'setting_key' => $key, 'setting_value' => $value, 'setting_group' => 'general', 'is_public' => 0 ]); } } /** * Get all settings by group */ function getSettingsByGroup($group = null) { $db = Database::getInstance(); $conditions = []; if ($group !== null) { $conditions['setting_group'] = $group; } return $db->select('settings', $conditions, '*', 'setting_group, setting_key'); } // ============================================= // CALCULATION FUNCTIONS // ============================================= /** * Calculate loan interest */ function calculateLoanInterest($principal, $interestRate, $term, $termUnit, $interestType = 'reducing') { if ($interestType === 'flat') { // Flat interest: (Principal * Rate * Time) / 100 if ($termUnit === 'months') { return ($principal * $interestRate * $term) / (12 * 100); } else { return ($principal * $interestRate * $term) / 100; } } else { // Reducing balance (simplified) $monthlyRate = $interestRate / 12 / 100; $totalInterest = 0; $remainingPrincipal = $principal; for ($i = 0; $i < $term; $i++) { $monthlyInterest = $remainingPrincipal * $monthlyRate; $totalInterest += $monthlyInterest; $monthlyPrincipal = $principal / $term; $remainingPrincipal -= $monthlyPrincipal; } return $totalInterest; } } /** * Calculate installment amount */ function calculateInstallment($principal, $interestRate, $term, $termUnit, $interestType = 'reducing') { $totalInterest = calculateLoanInterest($principal, $interestRate, $term, $termUnit, $interestType); $totalAmount = $principal + $totalInterest; return $totalAmount / $term; } /** * Calculate days between dates */ function calculateDaysBetween($date1, $date2) { $datetime1 = new DateTime($date1); $datetime2 = new DateTime($date2); $interval = $datetime1->diff($datetime2); return $interval->days; } /** * Calculate age from date of birth */ function calculateAge($dateOfBirth) { $birthDate = new DateTime($dateOfBirth); $today = new DateTime(); return $today->diff($birthDate)->y; } // ============================================= // STRING AND ARRAY FUNCTIONS // ============================================= /** * Convert array to options for select dropdown */ function arrayToOptions($array, $valueKey = 'id', $labelKey = 'name', $selected = null) { $options = ''; foreach ($array as $item) { $value = $item[$valueKey] ?? ''; $label = $item[$labelKey] ?? ''; $isSelected = ($selected !== null && $value == $selected) ? ' selected' : ''; $options .= "<option value=\"$value\"$isSelected>$label</option>"; } return $options; } /** * Convert comma-separated string to array */ function stringToArray($string, $delimiter = ',') { if (empty($string)) { return []; } $array = explode($delimiter, $string); return array_map('trim', $array); } /** * Convert array to comma-separated string */ function arrayToString($array, $delimiter = ',') { if (empty($array)) { return ''; } return implode($delimiter, array_map('trim', $array)); } /** * Get value from array with default */ function getArrayValue($array, $key, $default = null) { return $array[$key] ?? $default; } /** * Sanitize array values */ function sanitizeArray($array) { $security = new Security(); return array_map([$security, 'sanitizeInput'], $array); } // ============================================= // URL AND ROUTING FUNCTIONS // ============================================= /** * Generate URL */ function url($path = '', $params = []) { $url = BASE_URL . '/' . ltrim($path, '/'); if (!empty($params)) { $url .= '?' . http_build_query($params); } return $url; } /** * Redirect to URL */ function redirect($url, $statusCode = 302) { header("Location: $url", true, $statusCode); exit; } /** * Get current URL */ function currentUrl() { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; return $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } /** * Get base URL */ function baseUrl() { return BASE_URL; } /** * Check if current route matches */ function isRoute($route) { $currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); return $currentPath === $route; } // ============================================= // SESSION AND FLASH MESSAGES // ============================================= /** * Set flash message */ function setFlashMessage($type, $message) { $_SESSION['flash_messages'][] = [ 'type' => $type, 'message' => $message, 'timestamp' => time() ]; } /** * Get flash messages */ function getFlashMessages() { $messages = $_SESSION['flash_messages'] ?? []; unset($_SESSION['flash_messages']); return $messages; } /** * Display flash messages */ function displayFlashMessages() { $messages = getFlashMessages(); if (empty($messages)) { return; } $html = ''; foreach ($messages as $message) { $alertClass = 'alert-' . $message['type']; $html .= "<div class=\"alert $alertClass alert-dismissible fade show\" role=\"alert\">"; $html .= htmlspecialchars($message['message']); $html .= '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'; $html .= '</div>'; } return $html; } /** * Set session data */ function setSessionData($key, $value) { $_SESSION[$key] = $value; } /** * Get session data */ function getSessionData($key, $default = null) { return $_SESSION[$key] ?? $default; } /** * Clear session data */ function clearSessionData($key) { unset($_SESSION[$key]); } // ============================================= // TEMPLATE FUNCTIONS // ============================================= /** * Include template with variables */ function includeTemplate($template, $variables = []) { extract($variables); include $template; } /** * Render template as string */ function renderTemplate($template, $variables = []) { ob_start(); includeTemplate($template, $variables); return ob_get_clean(); } /** * Get template path */ function templatePath($template) { return APP_ROOT . '/templates/' . $template . '.php'; } // ============================================= ## **Now let's create the essential module files:** ### **1. Login Module:** ```php modules/auth/login.php <?php /** * Login Page * Farmers Loan Management System */ // Check if already logged in if (isLoggedIn()) { redirect('/dashboard'); exit; } // Initialize variables $error = ''; $username = ''; $remember = false; // Handle form submission if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Validate CSRF token $security = new Security(); if (!$security->validateCsrfToken($_POST['csrf_token'] ?? '')) { $error = 'Invalid security token. Please try again.'; } else { // Sanitize inputs $username = $security->sanitizeInput($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $remember = isset($_POST['remember']); // Validate inputs if (empty($username) || empty($password)) { $error = 'Please enter username and password'; } else { // Validate login credentials $result = validateLogin($username, $password); if ($result['success']) { $user = $result['user']; // Check if two-factor authentication is enabled if ($user['two_factor_enabled']) { // Store user ID in session for 2FA verification $_SESSION['2fa_user_id'] = $user['id']; $_SESSION['2fa_username'] = $user['username']; // Redirect to 2FA verification redirect('/two-factor-verify'); exit; } // Login user loginUser($user['id'], $user['username'], $user['role_id'], $user['branch_id']); // Set remember me cookie if requested if ($remember) { setRememberMe($user['id']); } // Redirect to intended page or dashboard $redirectUrl = $_SESSION['redirect_url'] ?? '/dashboard'; unset($_SESSION['redirect_url']); redirect($redirectUrl); exit; } else { $error = $result['message']; // Show attempts remaining if available if (isset($result['attempts_remaining'])) { $error .= " ({$result['attempts_remaining']} attempts remaining)"; } } } } } // Generate CSRF token $csrf_token = (new Security())->generateCsrfToken(); // Include header require_once APP_ROOT . '/includes/header.php'; ?> <div class="container"> <div class="row justify-content-center"> <div class="col-md-6 col-lg-5"> <div class="card login-card shadow-sm"> <div class="card-header text-center bg-primary text-white"> <h4 class="mb-0"> <i class="fas fa-hand-holding-usd me-2"></i> <?php echo APP_NAME; ?> </h4> <small class="text-light">Loan Management System</small> </div> <div class="card-body p-4"> <?php if (!empty($error)): ?> <div class="alert alert-danger alert-dismissible fade show" role="alert"> <?php echo htmlspecialchars($error); ?> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> <?php endif; ?> <?php if (isset($_GET['success'])): ?> <div class="alert alert-success alert-dismissible fade show" role="alert"> <?php switch ($_GET['success']) { case 'registered': echo 'Registration successful! Please login.'; break; case 'password_reset': echo 'Password reset successful! Please login with your new password.'; break; case 'logged_out': echo 'You have been logged out successfully.'; break; default: echo 'Operation completed successfully.'; } ?> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> <?php endif; ?> <form method="POST" action="" id="loginForm"> <input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>"> <div class="mb-3"> <label for="username" class="form-label"> <i class="fas fa-user me-1"></i> Username or Email </label> <input type="text" class="form-control" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required autofocus> <div class="form-text">Enter your username or email address</div> </div> <div class="mb-3"> <label for="password" class="form-label"> <i class="fas fa-lock me-1"></i> Password </label> <div class="input-group"> <input type="password" class="form-control" id="password" name="password" required> <button type="button" class="btn btn-outline-secondary password-toggle" data-target="password"> <i class="fas fa-eye"></i> </button> </div> <div class="form-text"> <a href="/forgot-password" class="text-decoration-none"> <i class="fas fa-key me-1"></i> Forgot Password? </a> </div> </div> <div class="mb-3 form-check"> <input type="checkbox" class="form-check-input" id="remember" name="remember" <?php echo $remember ? 'checked' : ''; ?>> <label class="form-check-label" for="remember"> Remember me on this device </label> </div> <div class="d-grid gap-2"> <button type="submit" class="btn btn-primary btn-lg"> <i class="fas fa-sign-in-alt me-2"></i> Login </button> </div> </form> <div class="mt-4 text-center"> <p class="mb-2">Demo Credentials:</p> <div class="row"> <div class="col-6"> <div class="card bg-light mb-2"> <div class="card-body p-2"> <small class="text-muted">Admin</small><br> <strong>admin / admin123</strong> </div> </div> </div> <div class="col-6"> <div class="card bg-light mb-2"> <div class="card-body p-2"> <small class="text-muted">Loan Officer</small><br> <strong>officer / officer123</strong> </div> </div> </div> </div> </div> </div> <div class="card-footer text-center bg-light"> <small class="text-muted"> Version <?php echo APP_VERSION; ?> © <?php echo date('Y'); ?> <?php echo APP_NAME; ?> </small> </div> </div> <div class="text-center mt-3"> <a href="/install" class="text-decoration-none"> <i class="fas fa-cog me-1"></i> First Time Setup </a> </div> </div> </div> </div> <style> .login-card { margin-top: 5rem; border-radius: 10px; border: none; } .login-card .card-header { border-radius: 10px 10px 0 0 !important; padding: 1.5rem; } .login-card .card-body { padding: 2rem; } @media (max-width: 576px) { .login-card { margin-top: 2rem; } } </style> <script> document.addEventListener('DOMContentLoaded', function() { // Password toggle document.querySelectorAll('.password-toggle').forEach(button => { button.addEventListener('click', function() { const targetId = this.dataset.target; const input = document.getElementById(targetId); const icon = this.querySelector('i'); if (input.type === 'password') { input.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); } else { input.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } }); }); // Form validation document.getElementById('loginForm').addEventListener('submit', function(e) { const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value.trim(); if (!username || !password) { e.preventDefault(); alert('Please enter both username and password'); return false; } // Show loading state const submitBtn = this.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i> Logging in...'; submitBtn.disabled = true; // Re-enable button after 3 seconds in case of error setTimeout(() => { submitBtn.innerHTML = originalText; submitBtn.disabled = false; }, 3000); }); }); </script> <?php // Include footer require_once APP_ROOT . '/includes/footer.php'; ?>