You Are All Set to Go!
All you have to do now is upload your website files and start your journey. Check out how to do that below:
# KAYAL STORE BASIC - REPOSITORY ================================================================================ Project Name: Kayal Store basic Created: 2025-09-26 02:37:07 Last Updated: 2025-09-26 02:37:51 Source ZIP: public_html.zip Total Files: 30 Total Folders: 13 ================================================================================ ## FILE STRUCTURE ================================================================================ Kayal Store basic/ ├── api/ │ └── dashboard.php ├── assets/ │ ├── css/ │ │ ├── components.css │ │ ├── mobile.css │ │ └── style.css │ ├── images/ │ │ ├── icons │ │ └── screenshots │ └── js/ │ ├── charts.js │ ├── main.js │ └── mobile.js ├── config/ │ ├── config.php │ └── database.php ├── default.php ├── includes/ │ ├── footer.php │ ├── functions.php │ ├── header.php │ └── navigation.php ├── index.php ├── login.php ├── logout.php ├── manifest.json ├── pages/ │ ├── base.php │ ├── expenses.php │ ├── export_expenses.php │ ├── export_sales.php │ ├── investments.php │ ├── sales.php │ └── users.php ├── setup.php ├── simple_setup.php ├── sql/ │ └── database.sql ├── sw.js ├── test_connection.php └── uploads/ └── receipts ================================================================================ ## FILE CONTENTS ================================================================================ ### FILE 1: default.php - Type: PHP - Size: 15.99 KB - Path: . - Name: default.php ------------------------------------------------------------
All you have to do now is upload your website files and start your journey. Check out how to do that below:
Here's what's happening with your fish business today.
| Type | Description | Amount | Date | Party |
|---|---|---|---|---|
| • |
No segment data available
Fish Business Management
Click on any credential below to auto-fill the login form:
Could not create database tables. Please check:
Update config/database.php with correct credentials and refresh this page.
'); } // Check if admin user already exists try { $existingAdmin = fetchRow("SELECT id FROM users WHERE role = 'admin' LIMIT 1"); if ($existingAdmin) { die('An admin user already exists. Please delete this setup.php file for security.
Go to LoginFish Business Management System
setup.php file from your server for security reasons.
setup.php) from your server.
Error: ' . htmlspecialchars($e->getMessage()) . '
Please check your database credentials and try again.
Important: Delete this simple_setup.php file after setup is complete for security.
-------------------- END OF FILE -------------------- ### FILE 8: sw.js - Type: JS - Size: 14.63 KB - Path: . - Name: sw.js ------------------------------------------------------------ /** * Service Worker for Kayal Aqua PWA * Handles offline functionality and caching */ const CACHE_NAME = 'kayal-aqua-v1.0.0'; const API_CACHE_NAME = 'kayal-aqua-api-v1.0.0'; // Files to cache for offline functionality const STATIC_ASSETS = [ '/', '/index.php', '/login.php', '/assets/css/style.css', '/assets/css/mobile.css', '/assets/css/components.css', '/assets/js/main.js', '/assets/js/mobile.js', '/assets/js/charts.js', '/pages/dashboard.php', '/pages/sales.php', '/pages/expenses.php', '/pages/investments.php', '/pages/base.php', // Add manifest and icons '/manifest.json', '/assets/images/icons/icon-192x192.png', '/assets/images/icons/icon-512x512.png' ]; // API endpoints to cache const API_ENDPOINTS = [ '/api/dashboard.php' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('Service Worker: Installing...'); event.waitUntil( Promise.all([ // Cache static assets caches.open(CACHE_NAME).then((cache) => { console.log('Service Worker: Caching static assets'); return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { credentials: 'same-origin' }))); }), // Cache API endpoints caches.open(API_CACHE_NAME).then((cache) => { console.log('Service Worker: Caching API endpoints'); return Promise.all( API_ENDPOINTS.map(url => { return fetch(new Request(url, { credentials: 'same-origin' })) .then(response => { if (response.ok) { return cache.put(url, response.clone()); } }) .catch(error => { console.log('Service Worker: Failed to cache API endpoint', url, error); }); }) ); }) ]).then(() => { console.log('Service Worker: Installation complete'); // Take control immediately self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('Service Worker: Activating...'); event.waitUntil( Promise.all([ // Clean up old caches caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }), // Take control of all clients self.clients.claim() ]).then(() => { console.log('Service Worker: Activation complete'); }) ); }); // Fetch event - serve from cache when offline self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); // Only handle GET requests if (request.method !== 'GET') { return; } // Handle different types of requests if (url.pathname.startsWith('/api/')) { // API requests - network first, then cache event.respondWith(handleAPIRequest(request)); } else if (isStaticAsset(url.pathname)) { // Static assets - cache first, then network event.respondWith(handleStaticAsset(request)); } else if (isPageRequest(url.pathname)) { // Page requests - network first, then cache, then offline page event.respondWith(handlePageRequest(request)); } }); /** * Handle API requests with network-first strategy */ async function handleAPIRequest(request) { try { // Try network first const networkResponse = await fetch(request); if (networkResponse.ok) { // Update cache with fresh data const cache = await caches.open(API_CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: API network request failed, trying cache', error); // Try cache if network fails const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline response for API requests return new Response( JSON.stringify({ error: 'Offline', message: 'This feature requires an internet connection' }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); } } /** * Handle static assets with cache-first strategy */ async function handleStaticAsset(request) { // Try cache first const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Try network if not in cache try { const networkResponse = await fetch(request); if (networkResponse.ok) { // Add to cache for future use const cache = await caches.open(CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: Static asset request failed', error); // Return a fallback for failed static assets if (request.url.includes('.css')) { return new Response('/* Offline */', { headers: { 'Content-Type': 'text/css' } }); } if (request.url.includes('.js')) { return new Response('// Offline', { headers: { 'Content-Type': 'application/javascript' } }); } // For other assets, throw the error throw error; } } /** * Handle page requests with network-first strategy */ async function handlePageRequest(request) { try { // Try network first const networkResponse = await fetch(request); if (networkResponse.ok) { // Update cache with fresh content const cache = await caches.open(CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: Page network request failed, trying cache', error); // Try cache if network fails const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline page if nothing else works return getOfflinePage(); } } /** * Generate offline page */ function getOfflinePage() { const offlineHTML = `✅ PDO and PDO_MySQL extensions are loaded
'; } else { echo '❌ PDO or PDO_MySQL extension is missing
'; echo 'Contact your hosting provider to enable PDO_MySQL extension.
'; exit; } // Test 2: Database Connection echo '✅ Successfully connected to MySQL server
'; } catch (PDOException $e) { echo '❌ Failed to connect to MySQL server
'; echo 'Error: ' . htmlspecialchars($e->getMessage()) . '
'; echo '✅ Successfully accessed database: ' . $dbname . '
'; } catch (PDOException $e) { echo '⚠️ Database does not exist or no access: ' . $dbname . '
'; echo 'Error: ' . htmlspecialchars($e->getMessage()) . '
'; // Try to create the database echo '✅ Database created successfully!
'; } catch (PDOException $e2) { echo '❌ Failed to create database
'; echo 'Error: ' . htmlspecialchars($e2->getMessage()) . '
'; echo 'Please create the database manually in your hosting control panel.
'; exit; } } // Test 4: Check Tables echo '⚠️ No tables found. This is normal for initial setup.
'; echo 'Tables will be created when you run setup.php
'; } else { echo '✅ Found ' . count($tables) . ' tables:
'; echo '❌ Error checking tables: ' . htmlspecialchars($e->getMessage()) . '
'; } // Test 5: Test Write Permissions echo '✅ Database write permissions are working
'; } catch (PDOException $e) { echo '❌ Database write permissions failed
'; echo 'Error: ' . htmlspecialchars($e->getMessage()) . '
'; echo 'Your database user needs CREATE, INSERT, and DROP permissions.
'; } // Configuration Summary echo '| Setting | '; echo 'Value | '; echo '
|---|---|
| ' . htmlspecialchars($key) . ' | '; echo '' . htmlspecialchars($value) . ' | '; echo '
'; echo 'Success! Your database connection is working properly. '; echo 'You can now proceed to run setup.php to create your admin account.'; echo '
'; echo ''; echo 'Security Note: Please delete this test_connection.php file after setup is complete.'; echo '
'; echo ''; ?> -------------------- END OF FILE -------------------- ### FILE 10: api/dashboard.php - Type: PHP - Size: 12.61 KB - Path: api - Name: dashboard.php ------------------------------------------------------------ 'Unauthorized'], 401); } $action = $_GET['action'] ?? ''; $currentUser = getCurrentUser(); try { switch ($action) { case 'revenue_expenses': $data = getRevenueExpensesData(); sendJsonResponse(['success' => true, 'data' => $data]); break; case 'business_segments': $data = getBusinessSegmentsData(); sendJsonResponse(['success' => true, 'data' => $data]); break; case 'monthly_trends': $year = $_GET['year'] ?? date('Y'); $data = getMonthlyTrendsData($year); sendJsonResponse(['success' => true, 'data' => $data]); break; case 'cash_flow': $data = getCashFlowData(); sendJsonResponse(['success' => true, 'data' => $data]); break; case 'top_products': $data = getTopProductsData(); sendJsonResponse(['success' => true, 'data' => $data]); break; case 'dashboard_stats': $data = getDashboardStatsData(); sendJsonResponse(['success' => true, 'data' => $data]); break; default: sendJsonResponse(['error' => 'Invalid action'], 400); } } catch (Exception $e) { error_log('Dashboard API Error: ' . $e->getMessage()); sendJsonResponse(['error' => 'Internal server error'], 500); } /** * Get revenue vs expenses data for chart */ function getRevenueExpensesData() { $period = $_GET['period'] ?? 'year'; $currentUser = getCurrentUser(); // Determine date range based on period switch ($period) { case 'month': $startDate = date('Y-m-01'); $endDate = date('Y-m-t'); $groupBy = 'DAY'; $dateFormat = '%Y-%m-%d'; break; case 'quarter': $startDate = date('Y-m-01', strtotime('first day of -2 months')); $endDate = date('Y-m-t'); $groupBy = 'WEEK'; $dateFormat = '%Y-%u'; break; default: // year $startDate = date('Y-01-01'); $endDate = date('Y-12-31'); $groupBy = 'MONTH'; $dateFormat = '%Y-%m'; } // Base conditions for user filtering $userCondition = ''; $params = [$startDate, $endDate]; if ($currentUser['role'] !== 'admin') { $userCondition = ' AND created_by = ?'; $params[] = $currentUser['id']; } // Get revenue data $revenueQuery = " SELECT DATE_FORMAT(sale_date, '{$dateFormat}') as period, SUM(amount) as amount FROM sales WHERE sale_date BETWEEN ? AND ? {$userCondition} GROUP BY DATE_FORMAT(sale_date, '{$dateFormat}') ORDER BY period "; $revenueData = fetchAll($revenueQuery, $params); // Get expenses data $expensesQuery = " SELECT DATE_FORMAT(expense_date, '{$dateFormat}') as period, SUM(amount) as amount FROM expenses WHERE expense_date BETWEEN ? AND ? {$userCondition} GROUP BY DATE_FORMAT(expense_date, '{$dateFormat}') ORDER BY period "; $expensesData = fetchAll($expensesQuery, $params); // Combine and format data for Chart.js $periods = array_unique(array_merge( array_column($revenueData, 'period'), array_column($expensesData, 'period') )); sort($periods); $revenue = []; $expenses = []; $labels = []; foreach ($periods as $period) { $revenueAmount = 0; $expenseAmount = 0; foreach ($revenueData as $row) { if ($row['period'] === $period) { $revenueAmount = (float)$row['amount']; break; } } foreach ($expensesData as $row) { if ($row['period'] === $period) { $expenseAmount = (float)$row['amount']; break; } } $revenue[] = $revenueAmount; $expenses[] = $expenseAmount; // Format label based on period if ($period === 'month') { $labels[] = date('M j', strtotime($period)); } elseif ($period === 'quarter') { $labels[] = 'Week ' . date('W', strtotime($period . '-01')); } else { $labels[] = date('M Y', strtotime($period . '-01')); } } return [ 'labels' => $labels, 'datasets' => [ [ 'label' => 'Revenue', 'data' => $revenue, 'backgroundColor' => 'rgba(255, 193, 7, 0.2)', 'borderColor' => 'rgba(255, 193, 7, 1)', 'borderWidth' => 2, 'fill' => true ], [ 'label' => 'Expenses', 'data' => $expenses, 'backgroundColor' => 'rgba(220, 53, 69, 0.2)', 'borderColor' => 'rgba(220, 53, 69, 1)', 'borderWidth' => 2, 'fill' => true ] ] ]; } /** * Get business segments data for pie chart */ function getBusinessSegmentsData() { $currentUser = getCurrentUser(); $userCondition = ''; $params = []; if ($currentUser['role'] !== 'admin') { $userCondition = ' AND s.created_by = ?'; $params[] = $currentUser['id']; } $query = " SELECT bs.name as segment_name, COALESCE(SUM(s.amount), 0) as revenue FROM business_segments bs LEFT JOIN sales s ON bs.id = s.segment_id {$userCondition} WHERE bs.status = 'active' GROUP BY bs.id, bs.name HAVING revenue > 0 ORDER BY revenue DESC "; $data = fetchAll($query, $params); $labels = array_column($data, 'segment_name'); $amounts = array_map(function($row) { return (float)$row['revenue']; }, $data); $colors = [ 'rgba(30, 58, 95, 1)', // Primary Navy 'rgba(255, 193, 7, 1)', // Yellow 'rgba(40, 167, 69, 1)', // Success 'rgba(23, 162, 184, 1)', // Info 'rgba(220, 53, 69, 1)' // Danger ]; return [ 'labels' => $labels, 'datasets' => [ [ 'data' => $amounts, 'backgroundColor' => array_slice($colors, 0, count($labels)), 'borderColor' => '#fff', 'borderWidth' => 2 ] ] ]; } /** * Get monthly trends data */ function getMonthlyTrendsData($year) { $currentUser = getCurrentUser(); $userCondition = ''; $params = [$year]; if ($currentUser['role'] !== 'admin') { $userCondition = ' AND created_by = ?'; $params[] = $currentUser['id']; } $query = " SELECT MONTH(transaction_date) as month, MONTHNAME(transaction_date) as month_name, SUM(CASE WHEN type = 'revenue' THEN amount ELSE 0 END) as revenue, SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expenses FROM ( SELECT 'revenue' as type, amount, sale_date as transaction_date, created_by FROM sales UNION ALL SELECT 'expense' as type, amount, expense_date as transaction_date, created_by FROM expenses ) t WHERE YEAR(transaction_date) = ? {$userCondition} GROUP BY MONTH(transaction_date), MONTHNAME(transaction_date) ORDER BY MONTH(transaction_date) "; $data = fetchAll($query, $params); // Fill in missing months with zeros $months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; $revenue = array_fill(0, 12, 0); $expenses = array_fill(0, 12, 0); foreach ($data as $row) { $monthIndex = (int)$row['month'] - 1; $revenue[$monthIndex] = (float)$row['revenue']; $expenses[$monthIndex] = (float)$row['expenses']; } return [ 'labels' => $months, 'datasets' => [ [ 'label' => 'Revenue', 'data' => $revenue, 'borderColor' => 'rgba(30, 58, 95, 1)', 'backgroundColor' => 'rgba(30, 58, 95, 0.1)', 'tension' => 0.4, 'fill' => true ], [ 'label' => 'Profit', 'data' => array_map(function($r, $e) { return $r - $e; }, $revenue, $expenses), 'borderColor' => 'rgba(40, 167, 69, 1)', 'backgroundColor' => 'rgba(40, 167, 69, 0.1)', 'tension' => 0.4, 'fill' => true ] ] ]; } /** * Get cash flow data for bar chart */ function getCashFlowData() { $currentUser = getCurrentUser(); $userCondition = ''; $params = []; if ($currentUser['role'] !== 'admin') { $userCondition = ' AND created_by = ?'; $params[] = $currentUser['id']; } // Get last 4 weeks of data $query = " SELECT WEEK(transaction_date) as week_num, SUM(CASE WHEN type = 'revenue' THEN amount ELSE -amount END) as net_flow FROM ( SELECT 'revenue' as type, amount, sale_date as transaction_date, created_by FROM sales WHERE sale_date >= DATE_SUB(CURDATE(), INTERVAL 4 WEEK) UNION ALL SELECT 'expense' as type, amount, expense_date as transaction_date, created_by FROM expenses WHERE expense_date >= DATE_SUB(CURDATE(), INTERVAL 4 WEEK) ) t WHERE 1=1 {$userCondition} GROUP BY WEEK(transaction_date) ORDER BY WEEK(transaction_date) DESC LIMIT 4 "; $data = fetchAll($query, $params); $data = array_reverse($data); // Show oldest to newest $labels = []; $cashFlow = []; $colors = []; for ($i = 0; $i < count($data); $i++) { $labels[] = 'Week ' . ($i + 1); $amount = (float)$data[$i]['net_flow']; $cashFlow[] = $amount; $colors[] = $amount >= 0 ? 'rgba(40, 167, 69, 1)' : 'rgba(220, 53, 69, 1)'; } return [ 'labels' => $labels, 'datasets' => [ [ 'label' => 'Net Cash Flow', 'data' => $cashFlow, 'backgroundColor' => $colors, 'borderColor' => $colors, 'borderWidth' => 1 ] ] ]; } /** * Get top products/services data */ function getTopProductsData() { $currentUser = getCurrentUser(); $userCondition = ''; $params = []; if ($currentUser['role'] !== 'admin') { $userCondition = ' AND created_by = ?'; $params[] = $currentUser['id']; } $query = " SELECT title, SUM(amount) as total_revenue, COUNT(*) as sales_count FROM sales WHERE sale_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) {$userCondition} GROUP BY title ORDER BY total_revenue DESC LIMIT 5 "; $data = fetchAll($query, $params); $labels = array_column($data, 'title'); $revenues = array_map(function($row) { return (float)$row['total_revenue']; }, $data); $colors = [ 'rgba(30, 58, 95, 1)', 'rgba(255, 193, 7, 1)', 'rgba(40, 167, 69, 1)', 'rgba(23, 162, 184, 1)', 'rgba(220, 53, 69, 1)' ]; return [ 'labels' => $labels, 'datasets' => [ [ 'label' => 'Revenue', 'data' => $revenues, 'backgroundColor' => array_slice($colors, 0, count($labels)), 'borderColor' => '#fff', 'borderWidth' => 1 ] ] ]; } /** * Get dashboard statistics summary */ function getDashboardStatsData() { $currentUser = getCurrentUser(); $stats = getDashboardStats($currentUser['role'] !== 'admin' ? $currentUser['id'] : null); // Calculate growth rates (mock data for now - implement actual calculation) $growthRates = [ 'revenue_growth' => 12.5, 'expense_growth' => -5.2, 'profit_growth' => 18.3, 'transaction_growth' => 8.1 ]; return array_merge($stats, $growthRates); } ?> -------------------- END OF FILE -------------------- ### FILE 11: assets/css/components.css - Type: CSS - Size: 11.29 KB - Path: assets/css - Name: components.css ------------------------------------------------------------ /* Reusable Components for Kayal Aqua */ /* Modal Component */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1050; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .modal.active { opacity: 1; visibility: visible; } .modal-content { background: var(--white); border-radius: var(--border-radius-lg); width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-lg); transform: scale(0.9); transition: transform 0.3s ease; } .modal.active .modal-content { transform: scale(1); } .modal-header { padding: var(--space-lg); border-bottom: 1px solid var(--light-gray); display: flex; justify-content: space-between; align-items: center; background: var(--primary-navy); color: var(--white); border-top-left-radius: var(--border-radius-lg); border-top-right-radius: var(--border-radius-lg); } .modal-title { margin: 0; font-size: var(--font-size-lg); font-weight: 600; } .modal-close { background: none; border: none; color: var(--white); font-size: 1.5rem; cursor: pointer; padding: var(--space-sm); border-radius: var(--border-radius); } .modal-close:hover { background: rgba(255, 255, 255, 0.1); } .modal-body { padding: var(--space-lg); } .modal-footer { padding: var(--space-lg); border-top: 1px solid var(--light-gray); display: flex; justify-content: flex-end; gap: var(--space-md); } /* Tabs Component */ .tabs { margin-bottom: var(--space-lg); } .tab-nav { display: flex; border-bottom: 2px solid var(--light-gray); margin-bottom: var(--space-lg); overflow-x: auto; } .tab-nav-item { background: none; border: none; padding: var(--space-md) var(--space-lg); cursor: pointer; border-bottom: 2px solid transparent; color: var(--gray); font-weight: 500; white-space: nowrap; transition: all 0.3s ease; } .tab-nav-item.active { color: var(--primary-navy); border-bottom-color: var(--primary-yellow); } .tab-nav-item:hover { color: var(--primary-navy); background: var(--light-gray); } .tab-content { display: none; } .tab-content.active { display: block; animation: fadeIn 0.3s ease; } /* Accordion Component */ .accordion-item { border: 1px solid var(--light-gray); border-radius: var(--border-radius); margin-bottom: var(--space-sm); overflow: hidden; } .accordion-header { background: var(--white); border: none; width: 100%; padding: var(--space-lg); text-align: left; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 500; color: var(--primary-navy); transition: background 0.3s ease; } .accordion-header:hover { background: var(--light-gray); } .accordion-header.active { background: var(--primary-navy); color: var(--white); } .accordion-icon { transition: transform 0.3s ease; } .accordion-header.active .accordion-icon { transform: rotate(180deg); } .accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } .accordion-content.active { max-height: 500px; } .accordion-body { padding: var(--space-lg); background: var(--white); border-top: 1px solid var(--light-gray); } /* Progress Bar Component */ .progress { background: var(--light-gray); border-radius: var(--border-radius); height: 8px; overflow: hidden; margin-bottom: var(--space-sm); } .progress-bar { height: 100%; background: linear-gradient(90deg, var(--primary-yellow), var(--primary-yellow-dark)); transition: width 0.6s ease; border-radius: var(--border-radius); } .progress-text { font-size: var(--font-size-sm); color: var(--gray); text-align: center; } /* Badge Component */ .badge { display: inline-flex; align-items: center; padding: 0.25rem 0.5rem; font-size: var(--font-size-xs); font-weight: 500; border-radius: 0.375rem; text-transform: uppercase; letter-spacing: 0.5px; } .badge-primary { background: var(--primary-navy); color: var(--white); } .badge-secondary { background: var(--primary-yellow); color: var(--primary-navy); } .badge-success { background: var(--success); color: var(--white); } .badge-danger { background: var(--danger); color: var(--white); } .badge-warning { background: var(--warning); color: var(--primary-navy); } .badge-info { background: var(--info); color: var(--white); } .badge-outline { background: transparent; border: 1px solid currentColor; } /* Dropdown Component */ .dropdown { position: relative; display: inline-block; } .dropdown-toggle { background: var(--white); border: 1px solid #ced4da; padding: var(--space-md); border-radius: var(--border-radius); cursor: pointer; display: flex; align-items: center; gap: var(--space-sm); min-width: 150px; } .dropdown-toggle:hover { border-color: var(--primary-yellow); } .dropdown-menu { position: absolute; top: 100%; left: 0; background: var(--white); border: 1px solid #ced4da; border-radius: var(--border-radius); box-shadow: var(--shadow-md); min-width: 200px; z-index: 1000; opacity: 0; visibility: hidden; transform: translateY(-10px); transition: all 0.3s ease; } .dropdown.active .dropdown-menu { opacity: 1; visibility: visible; transform: translateY(0); } .dropdown-item { display: block; width: 100%; padding: var(--space-md); background: none; border: none; text-align: left; cursor: pointer; color: var(--dark-gray); text-decoration: none; transition: background 0.3s ease; } .dropdown-item:hover { background: var(--light-gray); } .dropdown-divider { height: 1px; background: var(--light-gray); margin: var(--space-xs) 0; } /* Tooltip Component */ .tooltip { position: relative; display: inline-block; } .tooltip-text { visibility: hidden; width: 120px; background: var(--primary-navy); color: var(--white); text-align: center; border-radius: var(--border-radius); padding: var(--space-sm); font-size: var(--font-size-xs); position: absolute; z-index: 1000; bottom: 125%; left: 50%; margin-left: -60px; opacity: 0; transition: opacity 0.3s; } .tooltip-text::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: var(--primary-navy) transparent transparent transparent; } .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; } /* Breadcrumb Component */ .breadcrumb { display: flex; flex-wrap: wrap; padding: var(--space-md) 0; margin-bottom: var(--space-lg); list-style: none; } .breadcrumb-item { display: flex; align-items: center; } .breadcrumb-item + .breadcrumb-item { padding-left: var(--space-sm); } .breadcrumb-item + .breadcrumb-item::before { content: "/"; color: var(--gray); padding-right: var(--space-sm); } .breadcrumb-link { color: var(--primary-navy); text-decoration: none; } .breadcrumb-link:hover { text-decoration: underline; } .breadcrumb-item.active { color: var(--gray); } /* Card with Image Component */ .image-card { background: var(--white); border-radius: var(--border-radius); overflow: hidden; box-shadow: var(--shadow-sm); transition: transform 0.3s ease, box-shadow 0.3s ease; } .image-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); } .image-card-img { width: 100%; height: 200px; object-fit: cover; background: var(--light-gray); } .image-card-body { padding: var(--space-lg); } .image-card-title { font-size: var(--font-size-lg); font-weight: 600; color: var(--primary-navy); margin: 0 0 var(--space-sm) 0; } .image-card-text { color: var(--gray); line-height: 1.5; } /* Timeline Component */ .timeline { position: relative; padding-left: var(--space-xl); } .timeline::before { content: ''; position: absolute; left: 15px; top: 0; height: 100%; width: 2px; background: var(--primary-yellow); } .timeline-item { position: relative; margin-bottom: var(--space-xl); } .timeline-marker { position: absolute; left: -25px; width: 12px; height: 12px; border-radius: 50%; background: var(--primary-yellow); border: 3px solid var(--white); box-shadow: 0 0 0 3px var(--primary-yellow); } .timeline-content { background: var(--white); border-radius: var(--border-radius); padding: var(--space-lg); box-shadow: var(--shadow-sm); } .timeline-date { color: var(--gray); font-size: var(--font-size-sm); margin-bottom: var(--space-sm); } .timeline-title { color: var(--primary-navy); font-weight: 600; margin-bottom: var(--space-sm); } /* Data Table Component */ .data-table-wrapper { background: var(--white); border-radius: var(--border-radius); box-shadow: var(--shadow-sm); overflow: hidden; } .data-table-header { padding: var(--space-lg); background: var(--light-gray); border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; } .data-table-title { font-size: var(--font-size-lg); font-weight: 600; color: var(--primary-navy); margin: 0; } .data-table-actions { display: flex; gap: var(--space-sm); } .data-table { width: 100%; border-collapse: collapse; } .data-table th { background: var(--primary-navy); color: var(--white); padding: var(--space-md); text-align: left; font-weight: 600; } .data-table td { padding: var(--space-md); border-bottom: 1px solid #dee2e6; } .data-table tbody tr:hover { background: var(--light-gray); } /* Empty State Component */ .empty-state { text-align: center; padding: var(--space-xxl); color: var(--gray); } .empty-state-icon { font-size: 3rem; margin-bottom: var(--space-lg); color: var(--primary-yellow); } .empty-state-title { font-size: var(--font-size-lg); color: var(--primary-navy); margin-bottom: var(--space-md); } .empty-state-text { margin-bottom: var(--space-lg); line-height: 1.5; } /* Filter Component */ .filter-bar { background: var(--white); padding: var(--space-lg); border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: var(--space-lg); } .filter-row { display: flex; flex-wrap: wrap; gap: var(--space-md); align-items: end; } .filter-group { flex: 1; min-width: 200px; } .filter-actions { display: flex; gap: var(--space-sm); } /* Status Indicator */ .status-indicator { display: inline-flex; align-items: center; gap: var(--space-xs); font-size: var(--font-size-sm); } .status-dot { width: 8px; height: 8px; border-radius: 50%; } .status-active .status-dot { background: var(--success); } .status-inactive .status-dot { background: var(--danger); } .status-pending .status-dot { background: var(--warning); } /* Loading States */ .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: loading 1.5s infinite; border-radius: var(--border-radius); } .skeleton-text { height: 1rem; margin-bottom: var(--space-sm); } .skeleton-title { height: 1.5rem; width: 60%; margin-bottom: var(--space-md); } .skeleton-paragraph { height: 1rem; margin-bottom: var(--space-xs); } .skeleton-paragraph:last-child { width: 40%; } @keyframes loading { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } -------------------- END OF FILE -------------------- ### FILE 12: assets/css/mobile.css - Type: CSS - Size: 8.05 KB - Path: assets/css - Name: mobile.css ------------------------------------------------------------ /* Mobile-specific enhancements for Kayal Aqua */ /* Touch-friendly interactions */ @media (max-width: 767px) { /* Ensure all clickable elements meet minimum touch target size */ .btn, .nav-link, .form-control, input[type="checkbox"], input[type="radio"] { min-height: 44px; min-width: 44px; } /* Improve form usability on mobile */ .form-control { font-size: 16px; /* Prevents zoom on iOS */ padding: 0.75rem; } /* Stack form elements on mobile */ .form-row { flex-direction: column; } .form-row .col { width: 100%; margin-bottom: var(--space-md); } /* Mobile-specific table styling */ .table-mobile { border: 0; } .table-mobile thead { border: none; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } .table-mobile tr { border-bottom: 3px solid var(--light-gray); display: block; margin-bottom: var(--space-md); background: var(--white); border-radius: var(--border-radius); padding: var(--space-md); box-shadow: var(--shadow-sm); } .table-mobile td { border: none; display: block; font-size: var(--font-size-sm); text-align: right; padding: var(--space-sm) 0; border-bottom: 1px solid #eee; } .table-mobile td:last-child { border-bottom: none; } .table-mobile td:before { content: attr(data-label) ": "; float: left; font-weight: bold; color: var(--primary-navy); } /* Mobile navigation improvements */ .mobile-nav-item { border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .mobile-nav-item:last-child { border-bottom: none; } /* Quick action buttons for mobile */ .mobile-quick-actions { position: fixed; bottom: 20px; right: 20px; z-index: 999; } .quick-action-btn { width: 56px; height: 56px; border-radius: 50%; background: var(--primary-yellow); color: var(--primary-navy); border: none; box-shadow: var(--shadow-lg); margin-bottom: var(--space-sm); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; cursor: pointer; transition: all 0.3s ease; } .quick-action-btn:hover { transform: scale(1.1); box-shadow: var(--shadow-lg); } /* Mobile-specific stats layout */ .stats-mobile { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); } .stat-card-mobile { padding: var(--space-md); text-align: center; } .stat-card-mobile .stat-value { font-size: var(--font-size-lg); } .stat-card-mobile .stat-label { font-size: 0.7rem; } /* Mobile form enhancements */ .mobile-form-group { position: relative; } .mobile-form-group .form-label { position: absolute; top: 50%; left: var(--space-md); transform: translateY(-50%); color: var(--gray); pointer-events: none; transition: all 0.3s ease; } .mobile-form-group .form-control:focus + .form-label, .mobile-form-group .form-control:not(:placeholder-shown) + .form-label { top: -8px; left: var(--space-sm); font-size: var(--font-size-xs); color: var(--primary-navy); background: var(--white); padding: 0 var(--space-xs); } /* Mobile card layouts */ .mobile-card-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); } .mobile-card { background: var(--white); border-radius: var(--border-radius); padding: var(--space-md); box-shadow: var(--shadow-sm); } .mobile-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md); padding-bottom: var(--space-sm); border-bottom: 1px solid var(--light-gray); } .mobile-card-title { font-size: var(--font-size-base); font-weight: 600; color: var(--primary-navy); margin: 0; } .mobile-card-action { background: none; border: none; color: var(--primary-yellow); font-size: 1.2rem; cursor: pointer; padding: var(--space-xs); } /* Mobile-specific utility classes */ .mobile-only { display: block !important; } .desktop-only { display: none !important; } .mobile-center { text-align: center; } .mobile-hidden { display: none; } /* Swipe gestures for mobile tables */ .swipe-container { overflow-x: auto; -webkit-overflow-scrolling: touch; } .swipe-item { min-width: 280px; display: inline-block; vertical-align: top; } /* Mobile dropdown menus */ .mobile-dropdown { position: relative; } .mobile-dropdown-content { display: none; position: absolute; background: var(--white); min-width: 200px; box-shadow: var(--shadow-md); border-radius: var(--border-radius); z-index: 1000; top: 100%; right: 0; } .mobile-dropdown.active .mobile-dropdown-content { display: block; } .mobile-dropdown-item { display: block; padding: var(--space-md); color: var(--dark-gray); text-decoration: none; border-bottom: 1px solid var(--light-gray); } .mobile-dropdown-item:last-child { border-bottom: none; } .mobile-dropdown-item:hover { background: var(--light-gray); } /* Mobile pagination */ .mobile-pagination { display: flex; justify-content: center; margin-top: var(--space-lg); } .mobile-pagination .btn { margin: 0 var(--space-xs); padding: var(--space-sm); min-width: 40px; } /* Mobile search bar */ .mobile-search { position: relative; margin-bottom: var(--space-lg); } .mobile-search-input { width: 100%; padding: var(--space-md); padding-right: 50px; border: 2px solid var(--primary-yellow); border-radius: 25px; font-size: var(--font-size-base); } .mobile-search-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: var(--primary-yellow); border: none; border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; color: var(--primary-navy); } /* Pull-to-refresh indicator */ .pull-refresh { text-align: center; padding: var(--space-lg); color: var(--gray); font-size: var(--font-size-sm); } /* Mobile-specific animations */ .mobile-slide-in { animation: slideInFromRight 0.3s ease; } @keyframes slideInFromRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .mobile-fade-in { animation: fadeIn 0.5s ease; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* Bottom sheet for mobile */ .bottom-sheet { position: fixed; bottom: 0; left: 0; right: 0; background: var(--white); border-top-left-radius: 20px; border-top-right-radius: 20px; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1); transform: translateY(100%); transition: transform 0.3s ease; z-index: 1002; max-height: 80vh; overflow-y: auto; } .bottom-sheet.active { transform: translateY(0); } .bottom-sheet-handle { width: 40px; height: 4px; background: var(--gray); border-radius: 2px; margin: var(--space-md) auto var(--space-lg); } .bottom-sheet-content { padding: 0 var(--space-lg) var(--space-lg); } } /* Tablet-specific adjustments */ @media (min-width: 768px) and (max-width: 991px) { .mobile-only { display: none !important; } .tablet-cols-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md); } .tablet-cols-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); } } /* Desktop view - hide mobile elements */ @media (min-width: 768px) { .mobile-only { display: none !important; } .desktop-only { display: block !important; } .mobile-quick-actions { display: none; } } -------------------- END OF FILE -------------------- ### FILE 13: assets/css/style.css - Type: CSS - Size: 11.6 KB - Path: assets/css - Name: style.css ------------------------------------------------------------ /* Kayal Aqua - Main Stylesheet */ /* Mobile-First Responsive Design */ /* CSS Variables for consistent theming */ :root { /* Primary Colors */ --primary-navy: #1e3a5f; --primary-yellow: #ffc107; --primary-navy-light: #2c4a6b; --primary-navy-dark: #152d47; --primary-yellow-light: #fff350; --primary-yellow-dark: #e6ac00; /* Neutral Colors */ --white: #ffffff; --light-gray: #f8f9fa; --gray: #6c757d; --dark-gray: #495057; --black: #212529; /* Status Colors */ --success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8; /* Spacing */ --space-xs: 0.25rem; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --space-xl: 2rem; --space-xxl: 3rem; /* Typography */ --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; --font-size-xs: 0.75rem; --font-size-sm: 0.875rem; --font-size-base: 1rem; --font-size-lg: 1.125rem; --font-size-xl: 1.25rem; --font-size-xxl: 1.5rem; /* Border radius */ --border-radius: 0.375rem; --border-radius-lg: 0.5rem; /* Shadows */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); } /* Reset and Base Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } html { font-size: 16px; scroll-behavior: smooth; } body { font-family: var(--font-family); font-size: var(--font-size-base); line-height: 1.6; color: var(--dark-gray); background-color: var(--light-gray); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Layout Components */ .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 var(--space-md); } .main-wrapper { min-height: 100vh; display: flex; flex-direction: column; } .content-area { flex: 1; padding: var(--space-md); } /* Header Styles */ .header { background: linear-gradient(135deg, var(--primary-navy) 0%, var(--primary-navy-dark) 100%); color: var(--white); padding: var(--space-md) 0; box-shadow: var(--shadow-md); position: sticky; top: 0; z-index: 1000; } .header-content { display: flex; justify-content: space-between; align-items: center; } .logo { font-size: var(--font-size-xl); font-weight: bold; color: var(--primary-yellow); text-decoration: none; } .logo:hover { color: var(--primary-yellow-light); } /* Navigation */ .nav-toggle { display: block; background: none; border: none; color: var(--white); font-size: 1.5rem; cursor: pointer; padding: var(--space-sm); } .main-nav { position: fixed; top: 0; left: -100%; width: 280px; height: 100vh; background: var(--primary-navy); transition: left 0.3s ease; z-index: 1001; overflow-y: auto; } .main-nav.active { left: 0; } .nav-header { padding: var(--space-lg); background: var(--primary-navy-dark); display: flex; justify-content: space-between; align-items: center; } .nav-close { background: none; border: none; color: var(--white); font-size: 1.5rem; cursor: pointer; } .nav-menu { list-style: none; padding: var(--space-md) 0; } .nav-item { margin-bottom: var(--space-xs); } .nav-link { display: flex; align-items: center; padding: var(--space-md) var(--space-lg); color: var(--white); text-decoration: none; transition: all 0.3s ease; } .nav-link:hover, .nav-link.active { background: var(--primary-yellow); color: var(--primary-navy); } .nav-icon { margin-right: var(--space-md); width: 20px; } /* Overlay for mobile menu */ .nav-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; display: none; } .nav-overlay.active { display: block; } /* Card Components */ .card { background: var(--white); border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: var(--space-lg); overflow: hidden; } .card-header { padding: var(--space-lg); background: var(--primary-navy); color: var(--white); border-bottom: 1px solid var(--light-gray); } .card-title { font-size: var(--font-size-lg); font-weight: 600; margin: 0; } .card-body { padding: var(--space-lg); } .card-footer { padding: var(--space-lg); background: var(--light-gray); border-top: 1px solid #dee2e6; } /* Grid System */ .row { display: flex; flex-wrap: wrap; margin: 0 calc(var(--space-md) * -0.5); } .col { flex: 1; padding: 0 calc(var(--space-md) * 0.5); margin-bottom: var(--space-md); } .col-12 { flex: 0 0 100%; } .col-6 { flex: 0 0 50%; } .col-4 { flex: 0 0 33.333333%; } .col-3 { flex: 0 0 25%; } /* Form Elements */ .form-group { margin-bottom: var(--space-lg); } .form-label { display: block; margin-bottom: var(--space-sm); font-weight: 500; color: var(--primary-navy); } .form-control { width: 100%; padding: var(--space-md); border: 1px solid #ced4da; border-radius: var(--border-radius); font-size: var(--font-size-base); background: var(--white); transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .form-control:focus { outline: none; border-color: var(--primary-yellow); box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.25); } .form-select { appearance: none; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); background-position: right 0.75rem center; background-repeat: no-repeat; background-size: 1.5em 1.5em; padding-right: 2.5rem; } /* Buttons */ .btn { display: inline-flex; align-items: center; justify-content: center; padding: var(--space-md) var(--space-lg); border: none; border-radius: var(--border-radius); font-size: var(--font-size-base); font-weight: 500; text-decoration: none; text-align: center; cursor: pointer; transition: all 0.3s ease; min-width: 44px; /* Accessibility - minimum touch target */ min-height: 44px; } .btn-primary { background: var(--primary-navy); color: var(--white); } .btn-primary:hover { background: var(--primary-navy-dark); color: var(--white); } .btn-secondary { background: var(--primary-yellow); color: var(--primary-navy); } .btn-secondary:hover { background: var(--primary-yellow-dark); color: var(--primary-navy); } .btn-success { background: var(--success); color: var(--white); } .btn-danger { background: var(--danger); color: var(--white); } .btn-outline { background: transparent; color: var(--primary-navy); border: 2px solid var(--primary-navy); } .btn-outline:hover { background: var(--primary-navy); color: var(--white); } .btn-sm { padding: var(--space-sm) var(--space-md); font-size: var(--font-size-sm); min-width: 36px; min-height: 36px; } .btn-lg { padding: var(--space-lg) var(--space-xl); font-size: var(--font-size-lg); } .btn-block { width: 100%; } /* Statistics Cards */ .stats-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); margin-bottom: var(--space-xl); } .stat-card { background: var(--white); padding: var(--space-lg); border-radius: var(--border-radius); box-shadow: var(--shadow-sm); text-align: center; transition: transform 0.3s ease, box-shadow 0.3s ease; } .stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); } .stat-value { font-size: var(--font-size-xxl); font-weight: bold; color: var(--primary-navy); display: block; } .stat-label { font-size: var(--font-size-sm); color: var(--gray); margin-top: var(--space-sm); } .stat-change { font-size: var(--font-size-xs); margin-top: var(--space-xs); } .stat-change.positive { color: var(--success); } .stat-change.negative { color: var(--danger); } /* Tables */ .table-responsive { overflow-x: auto; margin-bottom: var(--space-lg); } .table { width: 100%; margin-bottom: 0; background: var(--white); border-collapse: collapse; } .table th, .table td { padding: var(--space-md); text-align: left; border-bottom: 1px solid #dee2e6; } .table th { background: var(--primary-navy); color: var(--white); font-weight: 600; white-space: nowrap; } .table tbody tr:hover { background: var(--light-gray); } /* Utilities */ .text-center { text-align: center; } .text-right { text-align: right; } .text-left { text-align: left; } .text-primary { color: var(--primary-navy); } .text-secondary { color: var(--primary-yellow); } .text-success { color: var(--success); } .text-danger { color: var(--danger); } .text-warning { color: var(--warning); } .text-info { color: var(--info); } .text-muted { color: var(--gray); } .bg-primary { background-color: var(--primary-navy); } .bg-secondary { background-color: var(--primary-yellow); } .bg-light { background-color: var(--light-gray); } .d-none { display: none; } .d-block { display: block; } .d-flex { display: flex; } .d-inline-flex { display: inline-flex; } .justify-center { justify-content: center; } .justify-between { justify-content: space-between; } .justify-end { justify-content: flex-end; } .items-center { align-items: center; } .items-end { align-items: flex-end; } .m-0 { margin: 0; } .mt-1 { margin-top: var(--space-xs); } .mb-1 { margin-bottom: var(--space-xs); } .mt-2 { margin-top: var(--space-sm); } .mb-2 { margin-bottom: var(--space-sm); } .mt-3 { margin-top: var(--space-md); } .mb-3 { margin-bottom: var(--space-md); } .mt-4 { margin-top: var(--space-lg); } .mb-4 { margin-bottom: var(--space-lg); } .p-0 { padding: 0; } .p-1 { padding: var(--space-xs); } .p-2 { padding: var(--space-sm); } .p-3 { padding: var(--space-md); } .p-4 { padding: var(--space-lg); } /* Footer */ .footer { background: var(--primary-navy); color: var(--white); text-align: center; padding: var(--space-lg) 0; margin-top: auto; } /* Loading Spinner */ .spinner { width: 40px; height: 40px; border: 4px solid var(--light-gray); border-top: 4px solid var(--primary-yellow); border-radius: 50%; animation: spin 1s linear infinite; margin: var(--space-lg) auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Alert Messages */ .alert { padding: var(--space-md); margin-bottom: var(--space-lg); border: 1px solid transparent; border-radius: var(--border-radius); } .alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; } .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; } .alert-warning { color: #856404; background-color: #fff3cd; border-color: #ffeaa7; } .alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; } /* Responsive Design */ @media (min-width: 576px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 768px) { .container { padding: 0 var(--space-lg); } .content-area { padding: var(--space-xl); } .stats-grid { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .nav-toggle { display: none; } .main-nav { position: static; width: auto; height: auto; background: transparent; display: flex; align-items: center; } .nav-header { display: none; } .nav-menu { display: flex; padding: 0; margin: 0; } .nav-item { margin-bottom: 0; margin-left: var(--space-md); } .nav-link { padding: var(--space-sm) var(--space-md); border-radius: var(--border-radius); } } @media (min-width: 992px) { .col-lg-12 { flex: 0 0 100%; } .col-lg-6 { flex: 0 0 50%; } .col-lg-4 { flex: 0 0 33.333333%; } .col-lg-3 { flex: 0 0 25%; } .col-lg-2 { flex: 0 0 16.666667%; } } -------------------- END OF FILE -------------------- ### FILE 14: assets/js/charts.js - Type: JS - Size: 18.48 KB - Path: assets/js - Name: charts.js ------------------------------------------------------------ /** * Charts and Data Visualization for Kayal Aqua * Mobile-responsive charts using Chart.js */ class KayalAquaCharts { constructor() { this.charts = {}; this.colors = { primary: '#1e3a5f', secondary: '#ffc107', success: '#28a745', danger: '#dc3545', warning: '#ffc107', info: '#17a2b8', light: '#f8f9fa', dark: '#495057' }; this.init(); } init() { // Initialize charts when DOM is ready if (typeof Chart !== 'undefined') { this.setupChartDefaults(); this.initializeAllCharts(); } else { // Load Chart.js if not already loaded this.loadChartJS().then(() => { this.setupChartDefaults(); this.initializeAllCharts(); }); } } async loadChartJS() { return new Promise((resolve, reject) => { if (typeof Chart !== 'undefined') { resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } setupChartDefaults() { Chart.defaults.font.family = "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"; Chart.defaults.color = this.colors.dark; Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; // Mobile-specific defaults if (window.innerWidth <= 768) { Chart.defaults.font.size = 12; Chart.defaults.plugins.legend.labels.boxWidth = 12; Chart.defaults.plugins.legend.labels.padding = 10; } } initializeAllCharts() { // Revenue vs Expenses Chart this.initRevenueExpensesChart(); // Business Segments Pie Chart this.initBusinessSegmentsChart(); // Monthly Trends Line Chart this.initMonthlyTrendsChart(); // Cash Flow Chart this.initCashFlowChart(); // Top Products/Services Chart this.initTopProductsChart(); } initRevenueExpensesChart() { const canvas = document.getElementById('revenueExpensesChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); // Sample data - replace with actual data from API const data = { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], datasets: [ { label: 'Revenue', data: [65000, 59000, 80000, 81000, 56000, 75000], backgroundColor: this.colors.secondary + '80', borderColor: this.colors.secondary, borderWidth: 2, fill: true }, { label: 'Expenses', data: [28000, 48000, 40000, 35000, 40000, 45000], backgroundColor: this.colors.danger + '80', borderColor: this.colors.danger, borderWidth: 2, fill: true } ] }; this.charts.revenueExpenses = new Chart(ctx, { type: 'line', data: data, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '₹' + value.toLocaleString(); } } } }, plugins: { title: { display: true, text: 'Revenue vs Expenses', font: { size: 16, weight: 'bold' }, color: this.colors.primary }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ₹' + context.parsed.y.toLocaleString(); } } } }, interaction: { intersect: false, mode: 'index' } } }); } initBusinessSegmentsChart() { const canvas = document.getElementById('businessSegmentsChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const data = { labels: ['Fish Sales', 'Dry Fish Sales', 'Ornamental Fish', 'Fish Fry/Roast', 'Cutting Service'], datasets: [{ data: [35, 25, 15, 20, 5], backgroundColor: [ this.colors.primary, this.colors.secondary, this.colors.success, this.colors.info, this.colors.warning ], borderWidth: 2, borderColor: '#fff' }] }; this.charts.businessSegments = new Chart(ctx, { type: 'doughnut', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Revenue by Business Segment', font: { size: 16, weight: 'bold' }, color: this.colors.primary }, legend: { position: window.innerWidth <= 768 ? 'bottom' : 'right', labels: { padding: window.innerWidth <= 768 ? 10 : 20, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((context.parsed / total) * 100).toFixed(1); return context.label + ': ' + percentage + '%'; } } } }, cutout: '60%' } }); } initMonthlyTrendsChart() { const canvas = document.getElementById('monthlyTrendsChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const data = { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], datasets: [ { label: 'Sales', data: [12, 19, 15, 25, 22, 30, 28, 35, 32, 38, 40, 45], borderColor: this.colors.primary, backgroundColor: this.colors.primary + '20', tension: 0.4, fill: true }, { label: 'Profit', data: [5, 8, 6, 12, 10, 15, 13, 18, 16, 20, 22, 25], borderColor: this.colors.success, backgroundColor: this.colors.success + '20', tension: 0.4, fill: true } ] }; this.charts.monthlyTrends = new Chart(ctx, { type: 'line', data: data, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '₹' + (value * 1000).toLocaleString(); } } } }, plugins: { title: { display: true, text: 'Monthly Trends', font: { size: 16, weight: 'bold' }, color: this.colors.primary }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ₹' + (context.parsed.y * 1000).toLocaleString(); } } } } } }); } initCashFlowChart() { const canvas = document.getElementById('cashFlowChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const data = { labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], datasets: [{ label: 'Cash Flow', data: [15000, -8000, 22000, -5000], backgroundColor: function(context) { const value = context.parsed.y; return value >= 0 ? '#28a745' : '#dc3545'; }, borderColor: function(context) { const value = context.parsed.y; return value >= 0 ? '#28a745' : '#dc3545'; }, borderWidth: 1 }] }; this.charts.cashFlow = new Chart(ctx, { type: 'bar', data: data, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '₹' + value.toLocaleString(); } } } }, plugins: { title: { display: true, text: 'Weekly Cash Flow', font: { size: 16, weight: 'bold' }, color: this.colors.primary }, legend: { display: false }, tooltip: { callbacks: { label: function(context) { const value = context.parsed.y; const type = value >= 0 ? 'Inflow' : 'Outflow'; return type + ': ₹' + Math.abs(value).toLocaleString(); } } } } } }); } initTopProductsChart() { const canvas = document.getElementById('topProductsChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const data = { labels: ['Fresh Pomfret', 'Dry Fish Mix', 'Gold Fish', 'Fish Fry', 'Cutting Service'], datasets: [{ label: 'Revenue', data: [25000, 18000, 12000, 15000, 8000], backgroundColor: [ this.colors.primary, this.colors.secondary, this.colors.success, this.colors.info, this.colors.warning ], borderWidth: 1, borderColor: '#fff' }] }; this.charts.topProducts = new Chart(ctx, { type: 'horizontalBar', data: data, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', scales: { x: { beginAtZero: true, ticks: { callback: function(value) { return '₹' + value.toLocaleString(); } } } }, plugins: { title: { display: true, text: 'Top Products/Services', font: { size: 16, weight: 'bold' }, color: this.colors.primary }, legend: { display: false }, tooltip: { callbacks: { label: function(context) { return context.label + ': ₹' + context.parsed.x.toLocaleString(); } } } } } }); } // Utility method to update chart data updateChartData(chartName, newData) { if (this.charts[chartName]) { this.charts[chartName].data = newData; this.charts[chartName].update(); } } // Method to destroy a chart destroyChart(chartName) { if (this.charts[chartName]) { this.charts[chartName].destroy(); delete this.charts[chartName]; } } // Method to resize charts for mobile resizeChartsForMobile() { Object.values(this.charts).forEach(chart => { chart.resize(); }); } // Method to get chart data from API async loadChartData(endpoint) { try { const response = await fetch(`api/${endpoint}`); const data = await response.json(); return data; } catch (error) { console.error('Error loading chart data:', error); return null; } } // Method to refresh all charts with new data async refreshAllCharts() { // Show loading state document.querySelectorAll('.chart-container').forEach(container => { container.classList.add('loading'); }); try { // Load data for each chart const revenueExpensesData = await this.loadChartData('dashboard.php?action=revenue_expenses'); const businessSegmentsData = await this.loadChartData('dashboard.php?action=business_segments'); const monthlyTrendsData = await this.loadChartData('dashboard.php?action=monthly_trends'); const cashFlowData = await this.loadChartData('dashboard.php?action=cash_flow'); const topProductsData = await this.loadChartData('dashboard.php?action=top_products'); // Update charts with new data if (revenueExpensesData) this.updateChartData('revenueExpenses', revenueExpensesData); if (businessSegmentsData) this.updateChartData('businessSegments', businessSegmentsData); if (monthlyTrendsData) this.updateChartData('monthlyTrends', monthlyTrendsData); if (cashFlowData) this.updateChartData('cashFlow', cashFlowData); if (topProductsData) this.updateChartData('topProducts', topProductsData); } catch (error) { console.error('Error refreshing charts:', error); } finally { // Hide loading state document.querySelectorAll('.chart-container').forEach(container => { container.classList.remove('loading'); }); } } // Method to export chart as image exportChartAsImage(chartName, filename) { if (this.charts[chartName]) { const link = document.createElement('a'); link.download = filename || `${chartName}_chart.png`; link.href = this.charts[chartName].toBase64Image(); link.click(); } } // Method to create mini dashboard charts for mobile createMiniCharts() { const miniChartContainers = document.querySelectorAll('.mini-chart'); miniChartContainers.forEach((container, index) => { const canvas = container.querySelector('canvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); // Sample data for mini charts const miniData = { labels: ['', '', '', '', ''], datasets: [{ data: [12, 19, 15, 25, 22], borderColor: this.colors.secondary, backgroundColor: this.colors.secondary + '40', borderWidth: 2, fill: true, tension: 0.4, pointRadius: 0 }] }; new Chart(ctx, { type: 'line', data: miniData, options: { responsive: true, maintainAspectRatio: false, scales: { x: { display: false }, y: { display: false } }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, elements: { point: { radius: 0 } } } }); }); } } // Initialize charts when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.kayalAquaCharts = new KayalAquaCharts(); }); // Handle window resize for chart responsiveness window.addEventListener('resize', () => { if (window.kayalAquaCharts) { window.kayalAquaCharts.resizeChartsForMobile(); } }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = KayalAquaCharts; } // Global function to initialize charts (called from main.js) function initializeCharts() { if (window.kayalAquaCharts) { window.kayalAquaCharts.refreshAllCharts(); } } -------------------- END OF FILE -------------------- ### FILE 15: assets/js/main.js - Type: JS - Size: 17.76 KB - Path: assets/js - Name: main.js ------------------------------------------------------------ /** * Main JavaScript for Kayal Aqua Fish Business Management * Mobile-First Responsive Application */ class KayalAquaApp { constructor() { this.init(); } init() { this.setupEventListeners(); this.initializeComponents(); this.handleResponsiveLayout(); } setupEventListeners() { // Navigation toggle document.addEventListener('click', (e) => { if (e.target.matches('.nav-toggle') || e.target.closest('.nav-toggle')) { this.toggleMobileNav(); } if (e.target.matches('.nav-close') || e.target.closest('.nav-close')) { this.closeMobileNav(); } if (e.target.matches('.nav-overlay')) { this.closeMobileNav(); } // Modal handlers if (e.target.matches('[data-modal]')) { this.openModal(e.target.dataset.modal); } if (e.target.matches('.modal-close') || e.target.matches('.modal-overlay')) { this.closeModal(); } // Dropdown handlers if (e.target.matches('.dropdown-toggle') || e.target.closest('.dropdown-toggle')) { this.toggleDropdown(e.target.closest('.dropdown')); } // Tab handlers if (e.target.matches('.tab-nav-item')) { this.switchTab(e.target); } // Accordion handlers if (e.target.matches('.accordion-header') || e.target.closest('.accordion-header')) { this.toggleAccordion(e.target.closest('.accordion-header')); } }); // Close dropdowns when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.dropdown')) { this.closeAllDropdowns(); } }); // Form submissions document.addEventListener('submit', (e) => { if (e.target.matches('form[data-ajax]')) { e.preventDefault(); this.handleAjaxForm(e.target); } }); // Window resize handler window.addEventListener('resize', () => { this.handleResponsiveLayout(); }); // Scroll handlers for mobile let lastScrollTop = 0; window.addEventListener('scroll', () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; // Hide/show header on scroll (mobile) if (window.innerWidth <= 768) { const header = document.querySelector('.header'); if (header) { if (scrollTop > lastScrollTop && scrollTop > 100) { header.style.transform = 'translateY(-100%)'; } else { header.style.transform = 'translateY(0)'; } } } lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; }); } initializeComponents() { // Initialize tooltips this.initializeTooltips(); // Initialize form validation this.initializeFormValidation(); // Initialize data tables this.initializeDataTables(); // Initialize charts if needed this.initializeCharts(); // Set active navigation this.setActiveNavigation(); } // Navigation Methods toggleMobileNav() { const nav = document.querySelector('.main-nav'); const overlay = document.querySelector('.nav-overlay'); if (nav && overlay) { nav.classList.toggle('active'); overlay.classList.toggle('active'); document.body.style.overflow = nav.classList.contains('active') ? 'hidden' : ''; } } closeMobileNav() { const nav = document.querySelector('.main-nav'); const overlay = document.querySelector('.nav-overlay'); if (nav && overlay) { nav.classList.remove('active'); overlay.classList.remove('active'); document.body.style.overflow = ''; } } setActiveNavigation() { const currentPage = window.location.pathname.split('/').pop() || 'index.php'; const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => { const href = link.getAttribute('href'); if (href === currentPage || (currentPage === '' && href === 'index.php')) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } // Modal Methods openModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add('active'); document.body.style.overflow = 'hidden'; } } closeModal() { const activeModal = document.querySelector('.modal.active'); if (activeModal) { activeModal.classList.remove('active'); document.body.style.overflow = ''; } } // Dropdown Methods toggleDropdown(dropdown) { if (dropdown) { dropdown.classList.toggle('active'); } } closeAllDropdowns() { const dropdowns = document.querySelectorAll('.dropdown.active'); dropdowns.forEach(dropdown => { dropdown.classList.remove('active'); }); } // Tab Methods switchTab(tabButton) { const tabContainer = tabButton.closest('.tabs'); if (!tabContainer) return; const targetTab = tabButton.dataset.tab; // Remove active class from all tabs and contents tabContainer.querySelectorAll('.tab-nav-item').forEach(tab => { tab.classList.remove('active'); }); tabContainer.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Add active class to clicked tab and corresponding content tabButton.classList.add('active'); const targetContent = tabContainer.querySelector(`#${targetTab}`); if (targetContent) { targetContent.classList.add('active'); } } // Accordion Methods toggleAccordion(header) { const content = header.nextElementSibling; const icon = header.querySelector('.accordion-icon'); if (content) { header.classList.toggle('active'); content.classList.toggle('active'); if (icon) { icon.style.transform = content.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)'; } } } // Form Methods handleAjaxForm(form) { const formData = new FormData(form); const url = form.action || window.location.href; this.showLoader(); fetch(url, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { this.hideLoader(); if (data.success) { this.showAlert(data.message || 'Operation completed successfully', 'success'); if (data.redirect) { setTimeout(() => { window.location.href = data.redirect; }, 1500); } if (data.reload) { setTimeout(() => { window.location.reload(); }, 1500); } } else { this.showAlert(data.message || 'An error occurred', 'danger'); } }) .catch(error => { this.hideLoader(); this.showAlert('Network error occurred', 'danger'); console.error('Error:', error); }); } initializeFormValidation() { const forms = document.querySelectorAll('form[data-validate]'); forms.forEach(form => { form.addEventListener('submit', (e) => { if (!this.validateForm(form)) { e.preventDefault(); } }); // Real-time validation const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { input.addEventListener('blur', () => { this.validateField(input); }); }); }); } validateForm(form) { let isValid = true; const inputs = form.querySelectorAll('[required], [data-validate]'); inputs.forEach(input => { if (!this.validateField(input)) { isValid = false; } }); return isValid; } validateField(field) { const value = field.value.trim(); const type = field.type; let isValid = true; let message = ''; // Clear previous errors this.clearFieldError(field); // Required validation if (field.hasAttribute('required') && !value) { isValid = false; message = 'This field is required'; } // Email validation if (type === 'email' && value && !this.isValidEmail(value)) { isValid = false; message = 'Please enter a valid email address'; } // Phone validation if (field.dataset.validate === 'phone' && value && !this.isValidPhone(value)) { isValid = false; message = 'Please enter a valid phone number'; } // Number validation if (type === 'number' && value) { const min = parseFloat(field.min); const max = parseFloat(field.max); const numValue = parseFloat(value); if (!isNaN(min) && numValue < min) { isValid = false; message = `Value must be at least ${min}`; } if (!isNaN(max) && numValue > max) { isValid = false; message = `Value must be no more than ${max}`; } } if (!isValid) { this.showFieldError(field, message); } return isValid; } showFieldError(field, message) { field.classList.add('is-invalid'); let errorDiv = field.parentNode.querySelector('.invalid-feedback'); if (!errorDiv) { errorDiv = document.createElement('div'); errorDiv.className = 'invalid-feedback'; field.parentNode.appendChild(errorDiv); } errorDiv.textContent = message; } clearFieldError(field) { field.classList.remove('is-invalid'); const errorDiv = field.parentNode.querySelector('.invalid-feedback'); if (errorDiv) { errorDiv.remove(); } } // Utility Methods isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } isValidPhone(phone) { const phoneRegex = /^[+]?[0-9\s\-\(\)]{10,}$/; return phoneRegex.test(phone); } showAlert(message, type = 'info', duration = 5000) { const alertContainer = document.getElementById('alert-container') || this.createAlertContainer(); const alert = document.createElement('div'); alert.className = `alert alert-${type} alert-dismissible`; alert.innerHTML = ` ${message} `; alertContainer.appendChild(alert); // Auto remove after duration if (duration > 0) { setTimeout(() => { if (alert.parentNode) { alert.remove(); } }, duration); } } createAlertContainer() { const container = document.createElement('div'); container.id = 'alert-container'; container.className = 'alert-container'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px; `; document.body.appendChild(container); return container; } showLoader() { let loader = document.getElementById('app-loader'); if (!loader) { loader = document.createElement('div'); loader.id = 'app-loader'; loader.innerHTML = ''; loader.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; z-index: 9999; `; document.body.appendChild(loader); } loader.style.display = 'flex'; } hideLoader() { const loader = document.getElementById('app-loader'); if (loader) { loader.style.display = 'none'; } } initializeTooltips() { // Tooltip functionality is handled by CSS :hover // This method can be extended for more complex tooltip behavior } initializeDataTables() { const tables = document.querySelectorAll('.data-table[data-sortable]'); tables.forEach(table => { this.makeTableSortable(table); }); } makeTableSortable(table) { const headers = table.querySelectorAll('th[data-sortable]'); headers.forEach(header => { header.style.cursor = 'pointer'; header.addEventListener('click', () => { this.sortTable(table, header); }); }); } sortTable(table, header) { const tbody = table.querySelector('tbody'); const rows = Array.from(tbody.querySelectorAll('tr')); const columnIndex = Array.from(header.parentNode.children).indexOf(header); const isAscending = !header.classList.contains('sort-asc'); // Clear all sort classes header.parentNode.querySelectorAll('th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); }); // Add sort class to current header header.classList.add(isAscending ? 'sort-asc' : 'sort-desc'); // Sort rows rows.sort((a, b) => { const aValue = a.children[columnIndex].textContent.trim(); const bValue = b.children[columnIndex].textContent.trim(); // Try to parse as numbers const aNum = parseFloat(aValue.replace(/[^\d.-]/g, '')); const bNum = parseFloat(bValue.replace(/[^\d.-]/g, '')); if (!isNaN(aNum) && !isNaN(bNum)) { return isAscending ? aNum - bNum : bNum - aNum; } // String comparison return isAscending ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); }); // Reorder DOM rows.forEach(row => tbody.appendChild(row)); } initializeCharts() { // Chart initialization will be handled in charts.js // This method triggers chart initialization if charts.js is loaded if (typeof initializeCharts === 'function') { initializeCharts(); } } handleResponsiveLayout() { const isMobile = window.innerWidth <= 768; // Handle table responsiveness const tables = document.querySelectorAll('.table'); tables.forEach(table => { if (isMobile) { table.classList.add('table-mobile'); } else { table.classList.remove('table-mobile'); } }); // Close mobile nav on resize to desktop if (!isMobile) { this.closeMobileNav(); } } // API Methods async apiCall(url, method = 'GET', data = null) { const options = { method, headers: { 'Content-Type': 'application/json', } }; if (data) { options.body = JSON.stringify(data); } try { const response = await fetch(url, options); return await response.json(); } catch (error) { console.error('API Error:', error); throw error; } } // Format currency for display formatCurrency(amount) { return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', minimumFractionDigits: 2 }).format(amount); } // Format date for display formatDate(dateString, options = {}) { const defaultOptions = { year: 'numeric', month: 'short', day: 'numeric' }; return new Date(dateString).toLocaleDateString('en-IN', { ...defaultOptions, ...options }); } // Debounce function for search inputs debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } } // Initialize the application when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.kayalAquaApp = new KayalAquaApp(); }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = KayalAquaApp; } -------------------- END OF FILE -------------------- ### FILE 16: assets/js/mobile.js - Type: JS - Size: 19.07 KB - Path: assets/js - Name: mobile.js ------------------------------------------------------------ /** * Mobile-specific JavaScript for Kayal Aqua * Enhanced mobile user experience */ class MobileEnhancements { constructor() { this.init(); } init() { if (this.isMobileDevice()) { this.setupMobileFeatures(); this.setupTouchGestures(); this.setupMobileNavigation(); this.setupPullToRefresh(); this.setupMobileSearch(); this.setupBottomSheet(); this.setupQuickActions(); } } isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } setupMobileFeatures() { // Prevent zoom on input focus this.preventInputZoom(); // Handle orientation change window.addEventListener('orientationchange', () => { setTimeout(() => { this.handleOrientationChange(); }, 100); }); // Handle viewport changes this.handleViewportChanges(); // Setup swipe gestures for navigation this.setupSwipeNavigation(); // Mobile-specific form enhancements this.enhanceMobileForms(); } preventInputZoom() { // Add viewport meta tag if not present let viewport = document.querySelector('meta[name="viewport"]'); if (!viewport) { viewport = document.createElement('meta'); viewport.name = 'viewport'; document.head.appendChild(viewport); } viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; // Ensure input font-size is at least 16px to prevent zoom const inputs = document.querySelectorAll('input, select, textarea'); inputs.forEach(input => { if (getComputedStyle(input).fontSize < '16px') { input.style.fontSize = '16px'; } }); } handleOrientationChange() { // Refresh layout after orientation change document.body.scrollTop = 0; document.documentElement.scrollTop = 0; // Trigger resize event window.dispatchEvent(new Event('resize')); // Close any open mobile menus if (window.kayalAquaApp) { window.kayalAquaApp.closeMobileNav(); } } handleViewportChanges() { let vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); window.addEventListener('resize', () => { vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); }); } setupTouchGestures() { let startX, startY, endX, endY; document.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true }); document.addEventListener('touchend', (e) => { if (!startX || !startY) return; endX = e.changedTouches[0].clientX; endY = e.changedTouches[0].clientY; const deltaX = startX - endX; const deltaY = startY - endY; // Only handle horizontal swipes that are significant if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 100) { if (deltaX > 0) { // Swipe left this.handleSwipeLeft(); } else { // Swipe right this.handleSwipeRight(); } } // Reset values startX = startY = endX = endY = null; }, { passive: true }); } handleSwipeLeft() { // Close mobile nav if open const nav = document.querySelector('.main-nav.active'); if (nav && window.kayalAquaApp) { window.kayalAquaApp.closeMobileNav(); } } handleSwipeRight() { // Open mobile nav if closed const nav = document.querySelector('.main-nav'); if (nav && !nav.classList.contains('active') && window.kayalAquaApp) { window.kayalAquaApp.toggleMobileNav(); } } setupSwipeNavigation() { // Add swipe indicators to swipeable elements const swipeContainers = document.querySelectorAll('.swipe-container'); swipeContainers.forEach(container => { container.style.scrollSnapType = 'x mandatory'; const items = container.querySelectorAll('.swipe-item'); items.forEach(item => { item.style.scrollSnapAlign = 'start'; }); }); } setupMobileNavigation() { // Add touch-friendly navigation const navItems = document.querySelectorAll('.nav-item'); navItems.forEach(item => { item.addEventListener('touchstart', function() { this.style.backgroundColor = 'rgba(255, 193, 7, 0.1)'; }, { passive: true }); item.addEventListener('touchend', function() { setTimeout(() => { this.style.backgroundColor = ''; }, 150); }, { passive: true }); }); } setupPullToRefresh() { let startY = 0; let currentY = 0; let pullThreshold = 100; let isRefreshing = false; const refreshIndicator = this.createRefreshIndicator(); document.addEventListener('touchstart', (e) => { if (window.scrollY === 0) { startY = e.touches[0].clientY; } }, { passive: true }); document.addEventListener('touchmove', (e) => { if (isRefreshing || window.scrollY > 0) return; currentY = e.touches[0].clientY; const pullDistance = currentY - startY; if (pullDistance > 0) { const pullRatio = Math.min(pullDistance / pullThreshold, 1); refreshIndicator.style.transform = `translateY(${pullDistance * 0.5}px)`; refreshIndicator.style.opacity = pullRatio; if (pullDistance > pullThreshold) { refreshIndicator.textContent = 'Release to refresh'; refreshIndicator.classList.add('ready-to-refresh'); } else { refreshIndicator.textContent = 'Pull to refresh'; refreshIndicator.classList.remove('ready-to-refresh'); } } }, { passive: true }); document.addEventListener('touchend', () => { if (isRefreshing) return; const pullDistance = currentY - startY; if (pullDistance > pullThreshold) { this.triggerRefresh(refreshIndicator); } else { this.resetRefreshIndicator(refreshIndicator); } startY = 0; currentY = 0; }, { passive: true }); } createRefreshIndicator() { const indicator = document.createElement('div'); indicator.className = 'pull-refresh'; indicator.textContent = 'Pull to refresh'; indicator.style.cssText = ` position: fixed; top: -50px; left: 0; right: 0; height: 50px; background: var(--primary-navy); color: var(--white); display: flex; align-items: center; justify-content: center; font-size: 14px; opacity: 0; transform: translateY(0); transition: transform 0.3s ease, opacity 0.3s ease; z-index: 1000; `; document.body.appendChild(indicator); return indicator; } triggerRefresh(indicator) { indicator.textContent = 'Refreshing...'; indicator.style.transform = 'translateY(50px)'; indicator.style.opacity = '1'; // Simulate refresh - replace with actual refresh logic setTimeout(() => { window.location.reload(); }, 1500); } resetRefreshIndicator(indicator) { indicator.style.transform = 'translateY(0)'; indicator.style.opacity = '0'; indicator.textContent = 'Pull to refresh'; indicator.classList.remove('ready-to-refresh'); } setupMobileSearch() { const searchInputs = document.querySelectorAll('.mobile-search-input'); searchInputs.forEach(input => { const searchBtn = input.parentElement.querySelector('.mobile-search-btn'); input.addEventListener('input', this.debounce((e) => { const query = e.target.value.trim(); if (query.length > 2) { this.performMobileSearch(query); } }, 300)); if (searchBtn) { searchBtn.addEventListener('click', () => { const query = input.value.trim(); if (query) { this.performMobileSearch(query); } }); } }); } performMobileSearch(query) { // Implement search functionality based on current page const currentPage = window.location.pathname.split('/').pop(); console.log(`Searching for: ${query} on page: ${currentPage}`); // Show search results this.showSearchResults(query); } showSearchResults(query) { // Create or update search results container let resultsContainer = document.getElementById('mobile-search-results'); if (!resultsContainer) { resultsContainer = document.createElement('div'); resultsContainer.id = 'mobile-search-results'; resultsContainer.className = 'mobile-search-results'; resultsContainer.style.cssText = ` position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; border-top: none; border-radius: 0 0 8px 8px; max-height: 300px; overflow-y: auto; z-index: 1000; display: none; `; const searchContainer = document.querySelector('.mobile-search'); if (searchContainer) { searchContainer.style.position = 'relative'; searchContainer.appendChild(resultsContainer); } } // Show loading state resultsContainer.innerHTML = 'Error: " . htmlspecialchars($e->getMessage()) . "
Please check:
Update the DB_PASS in config/database.php with your actual database password.