From b781a44ed51fbf61fb38a2f4fda40cea491201a3 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 8 Jan 2026 23:05:03 -0500 Subject: [PATCH] Added settings menu --- api/user_preferences.php | 125 +++++++++ assets/css/dashboard.css | 310 +++++++++++++++++++++ assets/js/settings.js | 142 ++++++++++ controllers/DashboardController.php | 33 ++- migrations/011_create_user_preferences.sql | 48 ++++ models/UserPreferencesModel.php | 98 +++++++ views/DashboardView.php | 124 +++++++++ views/TicketView.php | 123 ++++++++ 8 files changed, 996 insertions(+), 7 deletions(-) create mode 100644 api/user_preferences.php create mode 100644 assets/js/settings.js create mode 100644 migrations/011_create_user_preferences.sql create mode 100644 models/UserPreferencesModel.php diff --git a/api/user_preferences.php b/api/user_preferences.php new file mode 100644 index 0000000..5c67ae3 --- /dev/null +++ b/api/user_preferences.php @@ -0,0 +1,125 @@ + false, 'error' => 'Not authenticated']); + exit; +} + +$userId = $_SESSION['user']['user_id']; + +// Create database connection +$conn = new mysqli( + $GLOBALS['config']['DB_HOST'], + $GLOBALS['config']['DB_USER'], + $GLOBALS['config']['DB_PASS'], + $GLOBALS['config']['DB_NAME'] +); + +if ($conn->connect_error) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Database connection failed']); + exit; +} + +$prefsModel = new UserPreferencesModel($conn); + +// GET - Fetch all preferences for user +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + try { + $prefs = $prefsModel->getUserPreferences($userId); + echo json_encode(['success' => true, 'preferences' => $prefs]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Failed to fetch preferences']); + } + $conn->close(); + exit; +} + +// POST - Update a preference +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + + if (!isset($data['key']) || !isset($data['value'])) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Missing key or value']); + $conn->close(); + exit; + } + + $key = trim($data['key']); + $value = $data['value']; + + // Validate preference key (whitelist) + $validKeys = [ + 'rows_per_page', + 'default_status_filters', + 'table_density', + 'notifications_enabled', + 'sound_effects', + 'toast_duration' + ]; + + if (!in_array($key, $validKeys)) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Invalid preference key']); + $conn->close(); + exit; + } + + try { + $success = $prefsModel->setPreference($userId, $key, $value); + + // Also update cookie for rows_per_page for backwards compatibility + if ($key === 'rows_per_page') { + setcookie('ticketsPerPage', $value, time() + (86400 * 365), '/'); + } + + echo json_encode(['success' => $success]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Failed to save preference']); + } + $conn->close(); + exit; +} + +// DELETE - Delete a preference (optional endpoint) +if ($_SERVER['REQUEST_METHOD'] === 'DELETE') { + $data = json_decode(file_get_contents('php://input'), true); + + if (!isset($data['key'])) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Missing key']); + $conn->close(); + exit; + } + + try { + $success = $prefsModel->deletePreference($userId, $data['key']); + echo json_encode(['success' => $success]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Failed to delete preference']); + } + $conn->close(); + exit; +} + +// Method not allowed +http_response_code(405); +echo json_encode(['success' => false, 'error' => 'Method not allowed']); +$conn->close(); +?> diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index d8feca7..7d4692f 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -2604,3 +2604,313 @@ body.dark-mode select option { color: var(--terminal-cyan); text-shadow: 0 0 5px var(--terminal-cyan); } + +/* ======================================== + SETTINGS MODAL STYLES + ======================================== */ + +/* Settings Icon */ +.settings-icon { + background: transparent; + border: 2px solid var(--terminal-green); + color: var(--terminal-green); + font-size: 1.2rem; + padding: 0.25rem 0.5rem; + cursor: pointer; + font-family: var(--font-mono); + transition: all 0.3s ease; + border-radius: 0; + margin-left: 1rem; +} + +.settings-icon:hover { + background: var(--terminal-green); + color: var(--bg-primary); + box-shadow: var(--glow-green); +} + +/* Settings Modal Container */ +.settings-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; +} + +.settings-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); +} + +.settings-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--bg-secondary); + border: 2px solid var(--terminal-green); + box-shadow: 0 0 30px rgba(0, 255, 65, 0.5); + max-width: 800px; + max-height: 90vh; + overflow-y: auto; + font-family: var(--font-mono); + padding: 2rem; + animation: settingsSlideIn 0.3s ease; +} + +@keyframes settingsSlideIn { + from { + opacity: 0; + transform: translate(-50%, -60%); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} + +/* Settings Header */ +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid var(--terminal-green); +} + +.settings-header h3 { + color: var(--terminal-amber); + text-shadow: var(--glow-amber); + font-family: var(--font-mono); + font-size: 1.5rem; + margin: 0; +} + +.close-settings { + background: transparent; + border: 2px solid var(--terminal-green); + color: var(--terminal-green); + font-size: 1.5rem; + padding: 0.25rem 0.75rem; + cursor: pointer; + font-family: var(--font-mono); + transition: all 0.3s ease; +} + +.close-settings:hover { + background: var(--status-closed); + border-color: var(--status-closed); + color: var(--bg-primary); + box-shadow: 0 0 10px var(--status-closed); +} + +/* Settings Body */ +.settings-body { + margin-bottom: 2rem; +} + +/* Settings Sections */ +.settings-section { + margin-bottom: 2rem; + border: 2px solid var(--terminal-green); + padding: 1rem; +} + +.settings-section h4 { + color: var(--terminal-amber); + text-shadow: var(--glow-amber); + font-family: var(--font-mono); + margin-bottom: 1rem; + font-size: 1rem; +} + +/* Setting Rows */ +.setting-row { + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.setting-row:last-child { + margin-bottom: 0; +} + +.setting-row label { + color: var(--terminal-green); + font-family: var(--font-mono); + min-width: 180px; +} + +.setting-select { + flex: 1; + max-width: 200px; + font-family: var(--font-mono); + background: var(--bg-primary); + color: var(--terminal-green); + border: 2px solid var(--terminal-green); + padding: 0.5rem; + cursor: pointer; +} + +.setting-select:focus { + outline: none; + border-color: var(--terminal-amber); + box-shadow: 0 0 10px rgba(255, 193, 7, 0.3); +} + +/* Checkbox Group */ +.checkbox-group { + display: flex; + gap: 1rem; + flex-wrap: wrap; + flex: 1; +} + +.checkbox-group label { + min-width: auto; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.checkbox-group input[type="checkbox"] { + cursor: pointer; +} + +/* Keyboard Shortcuts Display */ +.shortcuts-list { + font-family: var(--font-mono); +} + +.shortcut-item { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid var(--terminal-green); +} + +.shortcut-item:last-child { + border-bottom: none; +} + +.shortcut-item kbd { + background: var(--bg-primary); + border: 2px solid var(--terminal-green); + padding: 0.25rem 0.5rem; + color: var(--terminal-amber); + text-shadow: var(--glow-amber); + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.shortcut-item span { + color: var(--terminal-green); +} + +/* User Info Grid */ +.user-info-grid { + display: grid; + grid-template-columns: 150px 1fr; + gap: 0.5rem 1rem; + font-family: var(--font-mono); + color: var(--terminal-green); +} + +.user-info-grid div { + padding: 0.25rem 0; +} + +/* Settings Footer */ +.settings-footer { + display: flex; + gap: 1rem; + justify-content: flex-end; + padding-top: 1rem; + border-top: 2px solid var(--terminal-green); +} + +.settings-footer .btn { + font-family: var(--font-mono); + padding: 0.75rem 1.5rem; + border: 2px solid; + cursor: pointer; + transition: all 0.3s ease; + background: transparent; +} + +.settings-footer .btn-primary { + color: var(--status-open); + border-color: var(--status-open); +} + +.settings-footer .btn-primary:hover { + background: var(--status-open); + color: var(--bg-primary); + box-shadow: 0 0 15px var(--status-open); +} + +.settings-footer .btn-secondary { + color: var(--terminal-green); + border-color: var(--terminal-green); +} + +.settings-footer .btn-secondary:hover { + background: var(--terminal-green); + color: var(--bg-primary); + box-shadow: var(--glow-green); +} + +/* Table Density Classes */ +.table-compact table { + font-size: 0.85rem; +} + +.table-compact th, +.table-compact td { + padding: 6px 10px; +} + +.table-comfortable table { + font-size: 1rem; +} + +.table-comfortable th, +.table-comfortable td { + padding: 16px 20px; +} + +/* Responsive Settings Modal */ +@media (max-width: 768px) { + .settings-content { + max-width: 95%; + padding: 1rem; + } + + .setting-row { + flex-direction: column; + align-items: flex-start; + } + + .setting-row label { + min-width: auto; + } + + .setting-select { + width: 100%; + max-width: 100%; + } + + .user-info-grid { + grid-template-columns: 1fr; + } +} diff --git a/assets/js/settings.js b/assets/js/settings.js new file mode 100644 index 0000000..6fab850 --- /dev/null +++ b/assets/js/settings.js @@ -0,0 +1,142 @@ +/** + * Settings Management System + * Handles loading, saving, and applying user preferences + */ + +let userPreferences = {}; + +// Load preferences on page load +async function loadUserPreferences() { + try { + const response = await fetch('/api/user_preferences.php'); + const data = await response.json(); + if (data.success) { + userPreferences = data.preferences; + applyPreferences(); + } + } catch (error) { + console.error('Error loading preferences:', error); + } +} + +// Apply preferences to UI +function applyPreferences() { + // Rows per page + const rowsPerPage = userPreferences.rows_per_page || '15'; + const rowsSelect = document.getElementById('rowsPerPage'); + if (rowsSelect) { + rowsSelect.value = rowsPerPage; + } + + // Default filters + const defaultFilters = (userPreferences.default_status_filters || 'Open,Pending,In Progress').split(','); + document.querySelectorAll('[name="defaultFilters"]').forEach(cb => { + cb.checked = defaultFilters.includes(cb.value); + }); + + // Table density + const density = userPreferences.table_density || 'normal'; + const densitySelect = document.getElementById('tableDensity'); + if (densitySelect) { + densitySelect.value = density; + } + document.body.classList.remove('table-compact', 'table-comfortable'); + if (density !== 'normal') { + document.body.classList.add(`table-${density}`); + } + + // Notifications + const notificationsCheckbox = document.getElementById('notificationsEnabled'); + if (notificationsCheckbox) { + notificationsCheckbox.checked = userPreferences.notifications_enabled !== '0'; + } + + const soundCheckbox = document.getElementById('soundEffects'); + if (soundCheckbox) { + soundCheckbox.checked = userPreferences.sound_effects !== '0'; + } + + // Toast duration + const toastDuration = userPreferences.toast_duration || '3000'; + const toastSelect = document.getElementById('toastDuration'); + if (toastSelect) { + toastSelect.value = toastDuration; + } +} + +// Save preferences +async function saveSettings() { + const prefs = { + rows_per_page: document.getElementById('rowsPerPage').value, + default_status_filters: Array.from(document.querySelectorAll('[name="defaultFilters"]:checked')) + .map(cb => cb.value).join(','), + table_density: document.getElementById('tableDensity').value, + notifications_enabled: document.getElementById('notificationsEnabled').checked ? '1' : '0', + sound_effects: document.getElementById('soundEffects').checked ? '1' : '0', + toast_duration: document.getElementById('toastDuration').value + }; + + try { + // Save each preference + for (const [key, value] of Object.entries(prefs)) { + const response = await fetch('/api/user_preferences.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key, value }) + }); + + const result = await response.json(); + if (!result.success) { + throw new Error(`Failed to save ${key}`); + } + } + + if (typeof toast !== 'undefined') { + toast.success('Preferences saved successfully!'); + } + closeSettingsModal(); + + // Reload page to apply new preferences + setTimeout(() => window.location.reload(), 1000); + } catch (error) { + if (typeof toast !== 'undefined') { + toast.error('Error saving preferences'); + } + console.error('Error saving preferences:', error); + } +} + +// Modal controls +function openSettingsModal() { + const modal = document.getElementById('settingsModal'); + if (modal) { + modal.style.display = 'block'; + loadUserPreferences(); + } +} + +function closeSettingsModal() { + const modal = document.getElementById('settingsModal'); + if (modal) { + modal.style.display = 'none'; + } +} + +// Keyboard shortcut to open settings (Alt+S) +document.addEventListener('keydown', (e) => { + if (e.altKey && e.key === 's') { + e.preventDefault(); + openSettingsModal(); + } + + // ESC to close modal + if (e.key === 'Escape') { + const modal = document.getElementById('settingsModal'); + if (modal && modal.style.display === 'block') { + closeSettingsModal(); + } + } +}); + +// Initialize on page load +document.addEventListener('DOMContentLoaded', loadUserPreferences); diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php index e9f0336..a3744e2 100644 --- a/controllers/DashboardController.php +++ b/controllers/DashboardController.php @@ -1,32 +1,51 @@ conn = $conn; $this->ticketModel = new TicketModel($conn); + $this->prefsModel = new UserPreferencesModel($conn); } public function index() { + // Get user ID for preferences + $userId = isset($_SESSION['user']['user_id']) ? $_SESSION['user']['user_id'] : null; + // Get query parameters $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; - $limit = isset($_COOKIE['ticketsPerPage']) ? (int)$_COOKIE['ticketsPerPage'] : 15; + + // Get rows per page from user preferences, fallback to cookie, then default + $limit = 15; + if ($userId) { + $limit = (int)$this->prefsModel->getPreference($userId, 'rows_per_page', 15); + } else if (isset($_COOKIE['ticketsPerPage'])) { + $limit = (int)$_COOKIE['ticketsPerPage']; + } + $sortColumn = isset($_GET['sort']) ? $_GET['sort'] : 'ticket_id'; $sortDirection = isset($_GET['dir']) ? $_GET['dir'] : 'desc'; $category = isset($_GET['category']) ? $_GET['category'] : null; $type = isset($_GET['type']) ? $_GET['type'] : null; - $search = isset($_GET['search']) ? trim($_GET['search']) : null; // ADD THIS LINE - - // Handle status filtering + $search = isset($_GET['search']) ? trim($_GET['search']) : null; + + // Handle status filtering with user preferences $status = null; if (isset($_GET['status']) && !empty($_GET['status'])) { $status = $_GET['status']; } else if (!isset($_GET['show_all'])) { - // Default: show Open, Pending, and In Progress (exclude Closed) - $status = 'Open,Pending,In Progress'; + // Get default status filters from user preferences + if ($userId) { + $status = $this->prefsModel->getPreference($userId, 'default_status_filters', 'Open,Pending,In Progress'); + } else { + // Default: show Open, Pending, and In Progress (exclude Closed) + $status = 'Open,Pending,In Progress'; + } } // If $_GET['show_all'] exists or no status param with show_all, show all tickets (status = null) diff --git a/migrations/011_create_user_preferences.sql b/migrations/011_create_user_preferences.sql new file mode 100644 index 0000000..736c9a8 --- /dev/null +++ b/migrations/011_create_user_preferences.sql @@ -0,0 +1,48 @@ +-- Migration 011: Create user_preferences table for persistent user settings +-- Stores user-specific preferences like rows per page, default filters, etc. + +CREATE TABLE IF NOT EXISTS user_preferences ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + preference_key VARCHAR(100) NOT NULL, + preference_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_user_pref (user_id, preference_key), + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); + +-- Default preferences for existing users +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'rows_per_page', '15' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'rows_per_page'); + +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'default_status_filters', 'Open,Pending,In Progress' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'default_status_filters'); + +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'table_density', 'normal' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'table_density'); + +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'notifications_enabled', '1' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'notifications_enabled'); + +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'sound_effects', '1' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'sound_effects'); + +INSERT INTO user_preferences (user_id, preference_key, preference_value) +SELECT user_id, 'toast_duration', '3000' FROM users +WHERE user_id NOT IN (SELECT user_id FROM user_preferences WHERE preference_key = 'toast_duration'); + +-- Verify table created +SELECT 'User Preferences Table Created' as info; +DESCRIBE user_preferences; + +-- Show count of preferences +SELECT 'Default Preferences Inserted' as info; +SELECT preference_key, COUNT(*) as user_count +FROM user_preferences +GROUP BY preference_key +ORDER BY preference_key; diff --git a/models/UserPreferencesModel.php b/models/UserPreferencesModel.php new file mode 100644 index 0000000..d326bad --- /dev/null +++ b/models/UserPreferencesModel.php @@ -0,0 +1,98 @@ +conn = $conn; + } + + /** + * Get all preferences for a user + * @param int $userId User ID + * @return array Associative array of preference_key => preference_value + */ + public function getUserPreferences($userId) { + $sql = "SELECT preference_key, preference_value + FROM user_preferences + WHERE user_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("i", $userId); + $stmt->execute(); + $result = $stmt->get_result(); + + $prefs = []; + while ($row = $result->fetch_assoc()) { + $prefs[$row['preference_key']] = $row['preference_value']; + } + return $prefs; + } + + /** + * Set or update a preference for a user + * @param int $userId User ID + * @param string $key Preference key + * @param string $value Preference value + * @return bool Success status + */ + public function setPreference($userId, $key, $value) { + $sql = "INSERT INTO user_preferences (user_id, preference_key, preference_value) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE preference_value = VALUES(preference_value)"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("iss", $userId, $key, $value); + return $stmt->execute(); + } + + /** + * Get a single preference value for a user + * @param int $userId User ID + * @param string $key Preference key + * @param mixed $default Default value if preference doesn't exist + * @return mixed Preference value or default + */ + public function getPreference($userId, $key, $default = null) { + $sql = "SELECT preference_value FROM user_preferences + WHERE user_id = ? AND preference_key = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("is", $userId, $key); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($row = $result->fetch_assoc()) { + return $row['preference_value']; + } + return $default; + } + + /** + * Delete a preference for a user + * @param int $userId User ID + * @param string $key Preference key + * @return bool Success status + */ + public function deletePreference($userId, $key) { + $sql = "DELETE FROM user_preferences + WHERE user_id = ? AND preference_key = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("is", $userId, $key); + return $stmt->execute(); + } + + /** + * Delete all preferences for a user + * @param int $userId User ID + * @return bool Success status + */ + public function deleteAllPreferences($userId) { + $sql = "DELETE FROM user_preferences WHERE user_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("i", $userId); + return $stmt->execute(); + } +} +?> diff --git a/views/DashboardView.php b/views/DashboardView.php index a5a235c..4228266 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -73,6 +73,7 @@ Admin + @@ -346,5 +347,128 @@ + + + + + + \ No newline at end of file diff --git a/views/TicketView.php b/views/TicketView.php index 8b98185..787b3a2 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -75,6 +75,7 @@ function formatDetails($details, $actionType) { Admin + @@ -322,5 +323,127 @@ function formatDetails($details, $actionType) { }; console.log('Ticket data loaded:', window.ticketData); + + + + + \ No newline at end of file