diff --git a/api/add_candidate.php b/api/add_candidate.php index b7e67d2..4415c33 100644 --- a/api/add_candidate.php +++ b/api/add_candidate.php @@ -31,7 +31,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { audit_log('Added candidate', 'candidates', $id); - header("Location: ../manage_candidates.php?position_id=$position_id&success=1"); + header("Location: ../candidate_management.php?success=1"); exit; } catch (Exception $e) { die($e->getMessage()); diff --git a/api/add_party.php b/api/add_party.php new file mode 100644 index 0000000..bcae228 --- /dev/null +++ b/api/add_party.php @@ -0,0 +1,29 @@ +prepare("INSERT INTO parties (id, election_id, name, description) VALUES (?, ?, ?, ?)"); + $stmt->execute([$id, $election_id, $name, $description]); + + audit_log('Added party', 'parties', $id); + + header("Location: ../candidate_management.php?success=1"); + exit; + } catch (Exception $e) { + die($e->getMessage()); + } +} diff --git a/api/add_position.php b/api/add_position.php index 6d9660b..996dec9 100644 --- a/api/add_position.php +++ b/api/add_position.php @@ -21,7 +21,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { audit_log('Added position', 'positions', $id); - header("Location: ../view_election.php?id=$election_id&success=1"); + header("Location: ../candidate_management.php?success=1"); exit; } catch (Exception $e) { die($e->getMessage()); diff --git a/assets/css/animations.css b/assets/css/animations.css new file mode 100644 index 0000000..0c45be0 --- /dev/null +++ b/assets/css/animations.css @@ -0,0 +1,19 @@ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fade-in { + animation: fadeIn 0.5s ease-out forwards; +} + +/* Delay for children */ +.animate-stagger > * { + opacity: 0; +} + +.animate-stagger > *:nth-child(1) { animation: fadeIn 0.5s ease-out 0.1s forwards; } +.animate-stagger > *:nth-child(2) { animation: fadeIn 0.5s ease-out 0.2s forwards; } +.animate-stagger > *:nth-child(3) { animation: fadeIn 0.5s ease-out 0.3s forwards; } +.animate-stagger > *:nth-child(4) { animation: fadeIn 0.5s ease-out 0.4s forwards; } +.animate-stagger > *:nth-child(5) { animation: fadeIn 0.5s ease-out 0.5s forwards; } diff --git a/assets/css/candidate_management.css b/assets/css/candidate_management.css new file mode 100644 index 0000000..fb46853 --- /dev/null +++ b/assets/css/candidate_management.css @@ -0,0 +1,251 @@ +.header-icon-container { + background: #eef2ff; + padding: 12px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Candidate Stats Grid */ +.candidate-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.candidate-stat-card { + background: #ffffff; + border: 1px solid #f3f4f6; + border-radius: 12px; + padding: 24px; +} + +.candidate-stat-label { + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + margin-bottom: 16px; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.candidate-stat-value { + font-size: 2.5rem; + font-weight: 800; + color: #2563eb; +} + +/* Distribution Grid */ +.distribution-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.distribution-card { + background: #ffffff; + border: 1px solid #f3f4f6; + border-radius: 12px; + padding: 24px; +} + +.distribution-header { + font-size: 0.875rem; + font-weight: 700; + color: #2563eb; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #f3f4f6; +} + +.distribution-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.distribution-item { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: #4b5563; +} + +.distribution-count { + font-weight: 700; + color: #1e293b; +} + +/* Filter Bar */ +.filter-bar { + padding: 24px; + display: flex; + gap: 16px; + align-items: flex-end; + background: #ffffff; + border-bottom: 1px solid #f3f4f6; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.filter-group label { + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.search-input-wrapper i { + position: absolute; + left: 12px; +} + +.search-input-wrapper input { + width: 100%; + padding: 10px 12px 10px 36px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + outline: none; + background: #f8fafc; +} + +.filter-group select { + padding: 10px 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + outline: none; + background: #ffffff; + color: #4b5563; +} + +/* Candidates Table */ +.candidates-table { + width: 100%; + border-collapse: collapse; +} + +.candidates-table th { + padding: 12px 24px; + text-align: left; + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + background: #f9fafb; + border-bottom: 1px solid #f3f4f6; + text-transform: uppercase; +} + +.candidates-table td { + padding: 16px 24px; + border-bottom: 1px solid #f3f4f6; + font-size: 0.875rem; + color: #1e293b; +} + +.candidate-info { + display: flex; + align-items: center; + gap: 12px; +} + +.candidate-avatar { + width: 32px; + height: 32px; + background: #eef2ff; + color: #4f46e5; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.75rem; +} + +.candidate-details { + display: flex; + flex-direction: column; +} + +.candidate-name { + font-weight: 600; + color: #1e293b; +} + +.candidate-sub { + font-size: 0.75rem; + color: #64748b; +} + +.position-badge { + background: #eef2ff; + color: #4f46e5; + padding: 4px 12px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.actions-cell { + display: flex; + gap: 12px; +} + +.actions-cell button { + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: #94a3b8; + transition: color 0.2s; +} + +.actions-cell button:hover { + color: #4f46e5; +} + +.actions-cell button i { + width: 16px; + height: 16px; +} + +.status-badge { + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.status-ongoing { + background: #dcfce7; + color: #166534; +} + +.status-ongoing::before { + content: ''; + width: 6px; + height: 6px; + background: #16a34a; + border-radius: 50%; +} diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..da19f6c --- /dev/null +++ b/assets/css/dashboard.css @@ -0,0 +1,394 @@ +@import 'animations.css'; + +:root { + --sidebar-width: 260px; + --sidebar-bg: #ffffff; + --sidebar-active-bg: #eef2ff; + --sidebar-active-text: #4f46e5; + --sidebar-text: #4b5563; + --top-header-height: 64px; + --accent-blue: #4f46e5; + --bg-light: #f9fafb; + --border-color: #f3f4f6; +} + +body.dashboard-body { + background-color: var(--bg-light); + display: flex; + min-height: 100vh; + font-family: 'Inter', sans-serif; + margin: 0; +} + +/* Sidebar Styles */ +.sidebar { + width: var(--sidebar-width); + background: var(--sidebar-bg); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + z-index: 100; +} + +.sidebar-header { + padding: 24px; + display: flex; + flex-direction: column; +} + +.sidebar-brand { + font-size: 1.1rem; + font-weight: 700; + color: #1e293b; + margin-bottom: 4px; +} + +.sidebar-subtitle { + font-size: 0.75rem; + color: #94a3b8; +} + +.sidebar-nav { + flex: 1; + padding: 12px; +} + +.nav-item { + display: flex; + align-items: center; + padding: 12px 16px; + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + margin-bottom: 4px; + font-weight: 500; + font-size: 0.875rem; + transition: all 0.2s; +} + +.nav-item:hover { + background: #f8fafc; +} + +.nav-item.active { + background: var(--sidebar-active-bg); + color: var(--sidebar-active-text); +} + +.nav-item i { + margin-right: 12px; + width: 18px; + height: 18px; + text-align: center; +} + +[data-lucide] { + width: 18px; + height: 18px; +} + +.sidebar-footer { + padding: 12px; + border-top: 1px solid var(--border-color); +} + +/* Main Content Area */ +.main-wrapper { + margin-left: var(--sidebar-width); + flex: 1; + display: flex; + flex-direction: column; +} + +.top-header { + height: var(--top-header-height); + background: #ffffff; + border-bottom: 1px solid var(--border-color); + padding: 0 32px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 90; +} + +.search-bar { + background: #f3f4f6; + border-radius: 8px; + padding: 8px 16px; + display: flex; + align-items: center; + width: 400px; +} + +.search-bar input { + background: transparent; + border: none; + outline: none; + margin-left: 8px; + width: 100%; + font-size: 0.875rem; +} + +.user-profile { + display: flex; + align-items: center; + gap: 12px; +} + +.user-info { + text-align: right; +} + +.user-name { + font-weight: 600; + font-size: 0.875rem; + color: #1e293b; +} + +.user-role { + font-size: 0.75rem; + color: #94a3b8; +} + +.user-avatar { + width: 36px; + height: 36px; + background: #e0e7ff; + color: #4f46e5; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; +} + +/* Dashboard Content */ +.dashboard-content { + padding: 32px; +} + +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 32px; +} + +.welcome-msg { + color: #64748b; + font-size: 0.875rem; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 32px; +} + +.stat-card { + background: #ffffff; + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; +} + +.stat-label { + color: #94a3b8; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + margin-bottom: 12px; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #1e293b; + margin-bottom: 12px; +} + +.stat-footer { + display: flex; + align-items: center; + font-size: 0.75rem; + font-weight: 500; +} + +.stat-footer.voters { color: #10b981; } +.stat-footer.candidates { color: #3b82f6; } +.stat-footer.votes { color: #10b981; } + +.stat-footer i { margin-right: 6px; } + +/* Analytics Charts */ +.analytics-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.analytics-card { + background: #ffffff; + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + display: flex; + flex-direction: column; +} + +.analytics-card .card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.analytics-card .card-title { + font-weight: 600; + font-size: 0.875rem; + color: #1e293b; +} + +.chart-filter { + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--border-color); + font-size: 0.75rem; + color: #4b5563; + outline: none; +} + +.chart-container { + position: relative; + height: 240px; + width: 100%; +} + +/* Table Section */ +.content-section { + background: #ffffff; + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 0; + overflow: hidden; +} + +.section-header { + padding: 20px 24px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.section-title { + font-weight: 600; + font-size: 1rem; + color: #1e293b; +} + +.btn-new-election { + background: #2563eb; + color: white; + padding: 8px 16px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; +} + +.election-table { + width: 100%; + border-collapse: collapse; +} + +.election-table th { + background: #f9fafb; + padding: 12px 24px; + text-align: left; + font-size: 0.7rem; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); +} + +.election-table td { + padding: 16px 24px; + border-bottom: 1px solid var(--border-color); + font-size: 0.875rem; +} + +.status-badge { + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-ongoing { + background: #dcfce7; + color: #166534; +} + +.status-preparing { + background: #fef3c7; + color: #92400e; +} + +.status-finished { + background: #f1f5f9; + color: #475569; +} + +.quick-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.select-status { + padding: 4px 8px; + border-radius: 4px; + border: 1px solid var(--border-color); + font-size: 0.75rem; + background: #f9fafb; +} + +.btn-update { + background: #2563eb; + color: white; + padding: 4px 12px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + border: none; + cursor: pointer; +} + +.flatlogic-badge { + position: fixed; + bottom: 16px; + right: 16px; + background: #ffffff; + border: 1px solid var(--border-color); + padding: 6px 12px; + border-radius: 6px; + font-size: 0.7rem; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} diff --git a/assets/css/officers_management.css b/assets/css/officers_management.css new file mode 100644 index 0000000..8295bbf --- /dev/null +++ b/assets/css/officers_management.css @@ -0,0 +1,195 @@ +/* Officer Management Specific Styles */ + +.officer-management-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-top: 24px; +} + +.officer-category-card { + background: white; + border-radius: 12px; + border: 1px solid #f3f4f6; + display: flex; + flex-direction: column; +} + +.category-header { + padding: 16px 20px; + border-bottom: 1px solid #f3f4f6; + display: flex; + justify-content: space-between; + align-items: center; +} + +.category-title { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + color: #1e293b; + font-size: 0.9375rem; +} + +.active-count { + background: #f0fdf4; + color: #16a34a; + font-size: 0.75rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 99px; +} + +.officer-list { + padding: 8px; + flex-grow: 1; +} + +.officer-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 12px; + border-radius: 8px; + transition: background-color 0.2s; +} + +.officer-item:hover { + background-color: #f8fafc; +} + +.officer-main-info { + display: flex; + align-items: center; + gap: 12px; +} + +.officer-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: #e0e7ff; + color: #4f46e5; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; +} + +.officer-details { + display: flex; + flex-direction: column; +} + +.officer-name { + font-size: 0.875rem; + font-weight: 500; + color: #1e293b; +} + +.officer-meta { + font-size: 0.75rem; + color: #64748b; +} + +.officer-actions { + display: flex; + gap: 8px; +} + +.officer-actions button { + background: transparent; + border: none; + color: #94a3b8; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; +} + +.officer-actions button:hover { + color: #4f46e5; + background: #f1f5f9; +} + +.empty-state { + padding: 40px 20px; + text-align: center; + color: #94a3b8; + font-size: 0.875rem; +} + +/* Registration Form Section */ +.registration-section { + background: white; + border-radius: 12px; + border: 1px solid #f3f4f6; + padding: 24px; + margin-bottom: 24px; +} + +.registration-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + color: #1e293b; + font-weight: 600; +} + +.form-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + align-items: flex-end; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-size: 0.75rem; + font-weight: 700; + color: #1e293b; + text-transform: uppercase; +} + +.form-group input, .form-group select { + padding: 10px 14px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + color: #1e293b; + outline: none; + transition: border-color 0.2s; +} + +.form-group input:focus, .form-group select:focus { + border-color: #4f46e5; +} + +.btn-save-officer { + background: #2563eb; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: 600; + font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + transition: background 0.2s; + height: 42px; +} + +.btn-save-officer:hover { + background: #1d4ed8; +} diff --git a/assets/css/reports_audit.css b/assets/css/reports_audit.css new file mode 100644 index 0000000..1f3e127 --- /dev/null +++ b/assets/css/reports_audit.css @@ -0,0 +1,119 @@ +/* Reports & Audit Styles */ + +.audit-table { + width: 100%; + border-collapse: collapse; + background: #ffffff; + border-radius: 12px; + overflow: hidden; +} + +.audit-table th { + padding: 12px 24px; + text-align: left; + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + background: #f9fafb; + border-bottom: 1px solid #f3f4f6; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.audit-table td { + padding: 16px 24px; + border-bottom: 1px solid #f3f4f6; + font-size: 0.875rem; + color: #1e293b; + vertical-align: middle; +} + +.audit-timestamp { + color: #475569; +} + +.audit-user-id { + font-weight: 500; +} + +.role-badge { + padding: 4px 10px; + border-radius: 6px; + font-size: 0.65rem; + font-weight: 700; + background: #dbeafe; + color: #2563eb; + text-transform: uppercase; +} + +.audit-action { + font-weight: 700; + color: #0f172a; +} + +.audit-details { + color: #64748b; +} + +/* Header section */ +.audit-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; +} + +.audit-title h1 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin: 0; +} + +.audit-subtitle { + font-size: 0.875rem; + color: #64748b; + font-weight: 500; +} + +/* Container for the table to add shadow and border radius */ +.table-container { + background: #ffffff; + border: 1px solid #f1f5f9; + border-radius: 12px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); +} + +.search-container { + margin-bottom: 24px; +} + +.audit-search-wrapper { + position: relative; + display: flex; + align-items: center; + max-width: 400px; +} + +.audit-search-wrapper i { + position: absolute; + left: 12px; + color: #94a3b8; +} + +.audit-search-wrapper input { + width: 100%; + padding: 10px 12px 10px 40px; + border: 1px solid #e2e8f0; + border-radius: 10px; + font-size: 0.875rem; + outline: none; + background: #f8fafc; + transition: all 0.2s; +} + +.audit-search-wrapper input:focus { + background: #ffffff; + border-color: #3b82f6; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); +} diff --git a/assets/css/voter_management.css b/assets/css/voter_management.css new file mode 100644 index 0000000..d43b5d9 --- /dev/null +++ b/assets/css/voter_management.css @@ -0,0 +1,338 @@ +.header-icon-container { + background: #eef2ff; + padding: 12px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Voter Stats Grid */ +.voter-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.voter-stat-card { + background: #ffffff; + border: 1px solid #f3f4f6; + border-radius: 12px; + padding: 24px; +} + +.voter-stat-label { + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + margin-bottom: 16px; + letter-spacing: 0.05em; +} + +.voter-stat-value { + font-size: 2.5rem; + font-weight: 800; +} + +/* Distribution Grid */ +.distribution-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.distribution-card { + background: #ffffff; + border: 1px solid #f3f4f6; + border-radius: 12px; + padding: 24px; +} + +.distribution-header { + font-size: 0.875rem; + font-weight: 700; + color: #2563eb; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #f3f4f6; +} + +.distribution-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.distribution-item { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: #4b5563; +} + +.distribution-count { + font-weight: 700; + color: #1e293b; +} + +/* Action Buttons */ +.btn-action { + padding: 10px 20px; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.btn-add { + background: #2563eb; + color: white; +} + +.btn-add:hover { background: #1d4ed8; } + +.btn-import { + background: #4f46e5; + color: white; +} + +.btn-import:hover { background: #4338ca; } + +/* Filter Bar */ +.filter-bar { + padding: 24px; + display: flex; + gap: 16px; + align-items: flex-end; + background: #ffffff; + border-bottom: 1px solid #f3f4f6; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.filter-group label { + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + letter-spacing: 0.05em; +} + +.search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.search-input-wrapper i { + position: absolute; + left: 12px; +} + +.search-input-wrapper input { + width: 100%; + padding: 10px 12px 10px 36px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + outline: none; + background: #f8fafc; +} + +.filter-group select { + padding: 10px 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + outline: none; + background: #ffffff; + color: #4b5563; +} + +/* Voters Table */ +.voters-table { + width: 100%; + border-collapse: collapse; +} + +.voters-table th { + padding: 12px 24px; + text-align: left; + font-size: 0.7rem; + font-weight: 700; + color: #64748b; + background: #f9fafb; + border-bottom: 1px solid #f3f4f6; +} + +.voters-table td { + padding: 16px 24px; + border-bottom: 1px solid #f3f4f6; + font-size: 0.875rem; + color: #1e293b; +} + +.status-indicator { + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.status-indicator.voted { + background: #dcfce7; + color: #166534; +} + +.status-indicator.pending { + background: #f1f5f9; + color: #475569; +} + +.actions-cell { + display: flex; + gap: 12px; +} + +.actions-cell button { + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: #94a3b8; + transition: color 0.2s; +} + +.actions-cell button:hover { + color: #4f46e5; +} + +.actions-cell button i { + width: 16px; + height: 16px; +} + +/* Modals */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(4px); + align-items: center; + justify-content: center; +} + +.modal-content { + background: white; + padding: 32px; + border-radius: 16px; + width: 100%; + max-width: 600px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.modal-header h2 { + font-size: 1.25rem; + color: #1e293b; + margin: 0; +} + +.close-btn { + background: none; + border: none; + font-size: 1.5rem; + color: #94a3b8; + cursor: pointer; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 24px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-size: 0.75rem; + font-weight: 600; + color: #64748b; +} + +.form-group input, .form-group select { + padding: 10px 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + outline: none; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +.btn-cancel { + padding: 10px 20px; + background: #f1f5f9; + color: #475569; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} + +.btn-submit { + padding: 10px 20px; + background: #2563eb; + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} + +.import-area { + border: 2px dashed #e2e8f0; + border-radius: 12px; + padding: 40px; + text-align: center; + background: #f8fafc; +} + +.import-area p { + font-size: 0.875rem; + color: #64748b; + margin: 0; +} diff --git a/assets/pasted-20260215-190109-c933c977.png b/assets/pasted-20260215-190109-c933c977.png new file mode 100644 index 0000000..c706368 Binary files /dev/null and b/assets/pasted-20260215-190109-c933c977.png differ diff --git a/assets/pasted-20260215-191054-6f35b633.png b/assets/pasted-20260215-191054-6f35b633.png new file mode 100644 index 0000000..9491140 Binary files /dev/null and b/assets/pasted-20260215-191054-6f35b633.png differ diff --git a/assets/pasted-20260215-191356-5299f94b.png b/assets/pasted-20260215-191356-5299f94b.png new file mode 100644 index 0000000..a986e04 Binary files /dev/null and b/assets/pasted-20260215-191356-5299f94b.png differ diff --git a/assets/pasted-20260215-192057-e6f6fe5d.png b/assets/pasted-20260215-192057-e6f6fe5d.png new file mode 100644 index 0000000..ade53a7 Binary files /dev/null and b/assets/pasted-20260215-192057-e6f6fe5d.png differ diff --git a/assets/pasted-20260215-192750-25aa33d0.png b/assets/pasted-20260215-192750-25aa33d0.png new file mode 100644 index 0000000..14d9f77 Binary files /dev/null and b/assets/pasted-20260215-192750-25aa33d0.png differ diff --git a/assets/pasted-20260215-193006-1ef97853.png b/assets/pasted-20260215-193006-1ef97853.png new file mode 100644 index 0000000..031b1bf Binary files /dev/null and b/assets/pasted-20260215-193006-1ef97853.png differ diff --git a/auth_helper.php b/auth_helper.php index 4523b7e..3272267 100644 --- a/auth_helper.php +++ b/auth_helper.php @@ -35,7 +35,8 @@ function uuid() { } function audit_log($action, $table = null, $record_id = null, $old = null, $new = null) { - $stmt = db()->prepare("INSERT INTO audit_logs (id, user_id, action, table_name, record_id, old_values, new_values) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $electionId = get_active_election_id(); + $stmt = db()->prepare("INSERT INTO audit_logs (id, user_id, action, table_name, record_id, old_values, new_values, election_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ uuid(), $_SESSION['user_id'] ?? null, @@ -43,6 +44,36 @@ function audit_log($action, $table = null, $record_id = null, $old = null, $new $table, $record_id, $old ? json_encode($old) : null, - $new ? json_encode($new) : null + $new ? json_encode($new) : null, + $electionId ]); } + +function get_active_election_id() { + if (isset($_GET['set_election_id'])) { + $_SESSION['active_election_id'] = $_GET['set_election_id']; + // Redirect to same page without the query param to keep URL clean + $url = strtok($_SERVER["REQUEST_URI"], '?'); + header("Location: " . $url); + exit; + } + + if (!isset($_SESSION['active_election_id'])) { + $election = db()->query("SELECT id FROM elections WHERE archived = FALSE ORDER BY created_at DESC LIMIT 1")->fetch(); + $_SESSION['active_election_id'] = $election['id'] ?? null; + } + + return $_SESSION['active_election_id']; +} + +function get_active_election() { + $id = get_active_election_id(); + if (!$id) return null; + $stmt = db()->prepare("SELECT * FROM elections WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); +} + +function get_all_elections() { + return db()->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll(); +} diff --git a/candidate_management.php b/candidate_management.php new file mode 100644 index 0000000..cc56e65 --- /dev/null +++ b/candidate_management.php @@ -0,0 +1,475 @@ +prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?"); +$totalCandidates->execute([$electionId]); +$totalCandidates = $totalCandidates->fetchColumn(); + +$uniquePositions = $pdo->prepare("SELECT COUNT(*) FROM positions WHERE election_id = ?"); +$uniquePositions->execute([$electionId]); +$uniquePositions = $uniquePositions->fetchColumn(); + +$activeParties = $pdo->prepare("SELECT COUNT(*) FROM parties WHERE election_id = ?"); +$activeParties->execute([$electionId]); +$activeParties = $activeParties->fetchColumn(); + +// Candidates by Position +$posStats = $pdo->prepare("SELECT p.name, COUNT(c.id) as count + FROM positions p LEFT JOIN candidates c ON p.id = c.position_id + WHERE p.election_id = ? GROUP BY p.id ORDER BY p.sort_order"); +$posStats->execute([$electionId]); +$posStats = $posStats->fetchAll(PDO::FETCH_ASSOC); + +// Candidates by Party +$partyStats = $pdo->prepare("SELECT p.name as party_name, COUNT(c.id) as count + FROM parties p LEFT JOIN candidates c ON p.name = c.party_name AND c.election_id = p.election_id + WHERE p.election_id = ? GROUP BY p.id ORDER BY count DESC"); +$partyStats->execute([$electionId]); +$partyStats = $partyStats->fetchAll(PDO::FETCH_ASSOC); + +// Filters +$search = $_GET['search'] ?? ''; +$filterPosition = $_GET['position'] ?? 'All Positions'; +$filterParty = $_GET['party'] ?? 'All Parties'; + +// Main Query +$query = "SELECT c.*, u.name as user_name, u.email as user_email, u.student_id, u.grade_level, u.track, p.name as position_name + FROM candidates c + JOIN users u ON c.user_id = u.id + JOIN positions p ON c.position_id = p.id + WHERE c.election_id = ?"; + +$params = [$electionId]; + +if ($search) { + $query .= " AND (u.name LIKE ? OR u.email LIKE ? OR c.party_name LIKE ?)"; + $params[] = "%$search%"; + $params[] = "%$search%"; + $params[] = "%$search%"; +} + +if ($filterPosition !== 'All Positions') { + $query .= " AND p.name = ?"; + $params[] = $filterPosition; +} + +if ($filterParty !== 'All Parties') { + $query .= " AND c.party_name = ?"; + $params[] = $filterParty; +} + +$query .= " ORDER BY p.sort_order, u.name"; + +$stmt = $pdo->prepare($query); +$stmt->execute($params); +$candidates = $stmt->fetchAll(); + +// Options for Modals/Filters +$allPositions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order"); +$allPositions->execute([$electionId]); +$allPositions = $allPositions->fetchAll(); + +$allParties = $pdo->prepare("SELECT * FROM parties WHERE election_id = ? ORDER BY name"); +$allParties->execute([$electionId]); +$allParties = $allParties->fetchAll(); + +$allVoters = $pdo->query("SELECT id, name, student_id FROM users WHERE role = 'Voter' ORDER BY name")->fetchAll(); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Candidate Management | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+
+ +
+
+

Candidate Management

+

Managing

+
+
+
+ + + +
+
+ +
+ + + +
+ + +
+
+
TOTAL CANDIDATES
+
+
+
+
UNIQUE POSITIONS
+
+
+
+
ACTIVE PARTIES
+
+
+
+ + +
+
+
Candidates by Position
+
+ +
+ + +
+ + +
No positions defined.
+ +
+
+
+
Candidates by Party
+
+ +
+ + +
+ + +
No parties defined.
+ +
+
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CANDIDATEPOSITIONPARTYGRADE/TRACKACTIONS
No candidates found in this election.
+
+
+ +
+
+ + | +
+
+
+ + +
+ Grade + +
+
+ + +
+
+
+
+ + + + + + + + + + + diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..163fcfa --- /dev/null +++ b/dashboard.php @@ -0,0 +1,260 @@ +prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND role_in_election = 'Voter'"); +$totalVoters->execute([$electionId]); +$totalVoters = $totalVoters->fetchColumn(); + +$totalCandidates = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?"); +$totalCandidates->execute([$electionId]); +$totalCandidates = $totalCandidates->fetchColumn(); + +$totalVotes = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?"); +$totalVotes->execute([$electionId]); +$totalVotes = $totalVotes->fetchColumn(); + +// Chart Data: Participation per Grade Level +$gradeStats = $pdo->prepare("SELECT COALESCE(u.grade_level, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count + FROM users u JOIN votes v ON u.id = v.voter_id + WHERE v.election_id = ? + GROUP BY u.grade_level ORDER BY u.grade_level"); +$gradeStats->execute([$electionId]); +$gradeStats = $gradeStats->fetchAll(PDO::FETCH_ASSOC); + +// Chart Data: Participation per Track +$trackStats = $pdo->prepare("SELECT COALESCE(u.track, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count + FROM users u JOIN votes v ON u.id = v.voter_id + WHERE v.election_id = ? + GROUP BY u.track"); +$trackStats->execute([$electionId]); +$trackStats = $trackStats->fetchAll(PDO::FETCH_ASSOC); + +// Chart Data: Participation per Section +$sectionStats = $pdo->prepare("SELECT u.track, u.section as label, COUNT(DISTINCT v.voter_id) as count + FROM users u JOIN votes v ON u.id = v.voter_id + WHERE v.election_id = ? + GROUP BY u.track, u.section"); +$sectionStats->execute([$electionId]); +$sectionStats = $sectionStats->fetchAll(PDO::FETCH_ASSOC); + +// Tracks for dropdown +$tracks = array_unique(array_column($sectionStats, 'track')); +sort($tracks); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Election Dashboard | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+

Election Dashboard

+
+ Active Election: +
+
+
+ + +
+
+
Total Voters
+
+ +
+
+
Total Candidates
+
+ +
+
+
Total Votes Cast
+
+ +
+
+ + +
+
+
+
Votes Per Grade Level
+
+
+ +
+
+
+
+
Votes Per Track
+
+
+ +
+
+
+ +
+
+
+
Votes Per Section
+ +
+
+ +
+
+
+
+
+ + + + diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql index 13ff25a..bd11883 100644 --- a/db/migrations/001_initial_schema.sql +++ b/db/migrations/001_initial_schema.sql @@ -101,6 +101,6 @@ CREATE TABLE audit_logs ( FOREIGN KEY (user_id) REFERENCES users(id) ); --- Insert a default admin (password is 'admin123') +-- Insert a default admin (password is 'Testing') INSERT INTO users (id, student_id, name, email, password_hash, role, access_level) -VALUES ('admin-uuid-1', '00-0000', 'Admin User', 'admin@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin', 4); +VALUES ('admin-uuid-1', '00-0000', 'Admin User', 'Admin@iloilonhs.edu.ph', '$2y$10$W70K9blIfzVSYbr/sEQUte3eyUejciAHmpubscltUNZbmpkPrF71K', 'Admin', 4); diff --git a/db/migrations/002_sample_data.sql b/db/migrations/002_sample_data.sql new file mode 100644 index 0000000..99c603d --- /dev/null +++ b/db/migrations/002_sample_data.sql @@ -0,0 +1,47 @@ +-- Sample Election Data +INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by) +VALUES ( + 'sample-election-uuid', + 'School Year 2028 Election', + 'General student council elections for the upcoming school year.', + 'Ongoing', + '2026-02-11 08:00:00', + '2026-02-18 17:00:00', + 'admin-uuid-1' +); + +-- Sample Voters (to match the "8" in the screenshot) +INSERT INTO users (id, student_id, name, email, password_hash, role) VALUES +('voter-1', '21-0001', 'John Doe', 'john@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-2', '21-0002', 'Jane Smith', 'jane@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-3', '21-0003', 'Bob Johnson', 'bob@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-4', '21-0004', 'Alice Brown', 'alice@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-5', '21-0005', 'Charlie Davis', 'charlie@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-6', '21-0006', 'Eve Wilson', 'eve@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-7', '21-0007', 'Frank Miller', 'frank@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'), +('voter-8', '21-0008', 'Grace Lee', 'grace@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'); + +-- Sample Candidates (to match the "15" in the screenshot) +-- We'll need some positions first +INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES +('pos-1', 'sample-election-uuid', 'President', 1, 1), +('pos-2', 'sample-election-uuid', 'Vice President', 1, 2); + +-- Insert 15 candidates (reusing voters for simplicity in this mockup) +INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES +('cand-1', 'sample-election-uuid', 'pos-1', 'voter-1', 'Unity Party', 1), +('cand-2', 'sample-election-uuid', 'pos-1', 'voter-2', 'Progress Party', 1), +('cand-3', 'sample-election-uuid', 'pos-2', 'voter-3', 'Unity Party', 1), +('cand-4', 'sample-election-uuid', 'pos-2', 'voter-4', 'Progress Party', 1), +('cand-5', 'sample-election-uuid', 'pos-1', 'voter-5', 'Independent', 1), +('cand-6', 'sample-election-uuid', 'pos-2', 'voter-6', 'Independent', 1), +('cand-7', 'sample-election-uuid', 'pos-1', 'voter-7', 'Students First', 1), +('cand-8', 'sample-election-uuid', 'pos-2', 'voter-8', 'Students First', 1), +('cand-9', 'sample-election-uuid', 'pos-1', 'admin-uuid-1', 'Faculty Choice', 1), +('cand-10', 'sample-election-uuid', 'pos-2', 'admin-uuid-1', 'Faculty Choice', 1), +-- Adding more to reach 15 +('cand-11', 'sample-election-uuid', 'pos-1', 'voter-1', 'Extra 1', 1), +('cand-12', 'sample-election-uuid', 'pos-2', 'voter-2', 'Extra 2', 1), +('cand-13', 'sample-election-uuid', 'pos-1', 'voter-3', 'Extra 3', 1), +('cand-14', 'sample-election-uuid', 'pos-2', 'voter-4', 'Extra 4', 1), +('cand-15', 'sample-election-uuid', 'pos-1', 'voter-5', 'Extra 5', 1); diff --git a/db/migrations/003_chart_data.sql b/db/migrations/003_chart_data.sql new file mode 100644 index 0000000..6e00675 --- /dev/null +++ b/db/migrations/003_chart_data.sql @@ -0,0 +1,39 @@ +-- Update sample voters with grade level, track, and section +UPDATE users SET grade_level = 11, track = 'STEM', section = 'A' WHERE id = 'voter-1'; +UPDATE users SET grade_level = 11, track = 'STEM', section = 'B' WHERE id = 'voter-2'; +UPDATE users SET grade_level = 11, track = 'ABM', section = 'C' WHERE id = 'voter-3'; +UPDATE users SET grade_level = 12, track = 'ABM', section = 'D' WHERE id = 'voter-4'; +UPDATE users SET grade_level = 12, track = 'HUMSS', section = 'E' WHERE id = 'voter-5'; +UPDATE users SET grade_level = 12, track = 'HUMSS', section = 'F' WHERE id = 'voter-6'; +UPDATE users SET grade_level = 11, track = 'GAS', section = 'G' WHERE id = 'voter-7'; +UPDATE users SET grade_level = 12, track = 'TVL', section = 'H' WHERE id = 'voter-8'; + +-- Insert some dummy votes so the charts aren't empty +-- We need to find the position and candidate IDs +-- President candidates: cand-1, cand-2, cand-5, cand-7, cand-9, cand-11, cand-13, cand-15 +-- Vice President candidates: cand-3, cand-4, cand-6, cand-8, cand-10, cand-12, cand-14 + +-- voter-1 votes for cand-1 (President) +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-1', '2026-02-11 10:00:00'); +-- voter-2 votes for cand-2 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-2', 'voter-2', '2026-02-12 11:00:00'); +-- voter-3 votes for cand-1 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-3', '2026-02-13 09:00:00'); +-- voter-4 votes for cand-5 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-5', 'voter-4', '2026-02-14 10:00:00'); +-- voter-5 votes for cand-7 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-7', 'voter-5', '2026-02-15 11:00:00'); +-- voter-6 votes for cand-1 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-6', '2026-02-15 12:00:00'); +-- voter-7 votes for cand-9 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-9', 'voter-7', '2026-02-15 13:00:00'); +-- voter-8 votes for cand-11 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) +VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-11', 'voter-8', '2026-02-15 14:00:00'); diff --git a/db/migrations/004_election_history_data.sql b/db/migrations/004_election_history_data.sql new file mode 100644 index 0000000..7f7f71e --- /dev/null +++ b/db/migrations/004_election_history_data.sql @@ -0,0 +1,22 @@ +-- Past Elections for History +INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by) VALUES +('hist-uuid-1', 'School Year 2027-2028 Election', 'Previous year elections.', 'Finished', '2027-02-10 08:00:00', '2027-02-11 17:00:00', 'admin-uuid-1'), +('hist-uuid-2', 'School Year 2026-2027 Election', 'Elections from 2 years ago.', 'Finished', '2026-02-10 08:00:00', '2026-02-11 17:00:00', 'admin-uuid-1'), +('hist-uuid-3', 'School Year 2022-2023 Election', 'Older elections.', 'Finished', '2022-09-01 08:00:00', '2022-09-02 17:00:00', 'admin-uuid-1'), +('hist-uuid-4', 'School Year 2020-2021 Election', 'Archived elections.', 'Finished', '2020-09-01 08:00:00', '2020-09-02 17:00:00', 'admin-uuid-1'); + +-- Positions for 2027-2028 +INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES +('pos-hist-1', 'hist-uuid-1', 'President', 1, 1), +('pos-hist-2', 'hist-uuid-1', 'Vice President', 1, 2); + +-- Candidates for 2027-2028 +INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES +('cand-hist-1', 'hist-uuid-1', 'pos-hist-1', 'voter-1', 'Unity Party', 1), +('cand-hist-2', 'hist-uuid-1', 'pos-hist-1', 'voter-2', 'Progress Party', 1); + +-- Votes for 2027-2028 +INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) VALUES +(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-1', 'voter-1', '2027-02-10 09:00:00'), +(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-1', 'voter-2', '2027-02-10 10:00:00'), +(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-2', 'voter-3', '2027-02-10 11:00:00'); diff --git a/db/migrations/005_voter_management_data.sql b/db/migrations/005_voter_management_data.sql new file mode 100644 index 0000000..fea8fa6 --- /dev/null +++ b/db/migrations/005_voter_management_data.sql @@ -0,0 +1,10 @@ +-- Migration 005: Additional Voter Data for Management View +INSERT INTO users (id, student_id, name, email, password_hash, grade_level, track, section, role) VALUES +(UUID(), '28-0001', 'John Doe', 'john.doe@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'STEM', 'A', 'Voter'), +(UUID(), '28-0002', 'Jane Smith', 'jane.smith@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ABM', 'A', 'Voter'), +(UUID(), '28-0003', 'Bob Wilson', 'bob.wilson@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'GAS', 'A', 'Voter'), +(UUID(), '28-0004', 'Alice Brown', 'alice.brown@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'HE', 'F', 'Voter'), +(UUID(), '28-0005', 'Charlie Davis', 'charlie.davis@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'HUMSS', 'A', 'Voter'), +(UUID(), '28-0006', 'Diana Prince', 'diana.prince@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ICT', 'A', 'Voter'), +(UUID(), '28-0007', 'Edward Norton', 'edward.norton@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'STEM', 'A', 'Voter'), +(UUID(), '28-0008', 'Fiona Gallagher', 'fiona.gallagher@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ICT', 'A', 'Voter'); diff --git a/db/migrations/006_candidate_management_data.sql b/db/migrations/006_candidate_management_data.sql new file mode 100644 index 0000000..4569439 --- /dev/null +++ b/db/migrations/006_candidate_management_data.sql @@ -0,0 +1,49 @@ +-- Refined 006 +SET FOREIGN_KEY_CHECKS = 0; +DELETE FROM candidates; +DELETE FROM positions; +SET FOREIGN_KEY_CHECKS = 1; + +INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES +('pos-gov', 'sample-election-uuid', 'Governor', 1, 1), +('pos-vgov', 'sample-election-uuid', 'Vice Governor', 1, 2), +('pos-sec', 'sample-election-uuid', 'Secretary', 1, 3), +('pos-pio', 'sample-election-uuid', 'PIO', 1, 4), +('pos-bm', 'sample-election-uuid', 'Board Member', 4, 5); + +-- Using different student IDs to avoid conflicts with 005 +INSERT INTO users (id, student_id, name, email, password_hash, role, grade_level, track, section) +VALUES +('cand-1-uuid', '29-7832', 'Kurt Leovince Tse Wing', 'kurtleovince06@gmail.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'A'), +('cand-2-uuid', '29-0002', 'Noah Padilla', 'noah.p@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'A'), +('cand-3-uuid', '29-0003', 'Liam Garcia', 'liam.g@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'B'), +('cand-4-uuid', '29-0004', 'Emma Wilson', 'emma.w@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'C'), +('cand-5-uuid', '29-0005', 'Olivia Martinez', 'olivia.m@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'D'), +('cand-6-uuid', '29-0006', 'James Brown', 'james.b@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'B'), +('cand-7-uuid', '29-0007', 'Sophia Davis', 'sophia.d@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'A'), +('cand-8-uuid', '29-0008', 'Mason Rodriguez', 'mason.r@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'A'), +('cand-9-uuid', '29-0009', 'Isabella Lopez', 'isabella.l@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'A'), +('cand-10-uuid', '29-0010', 'Ethan Wilson', 'ethan.w@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'C'), +('cand-11-uuid', '29-0011', 'Ava Moore', 'ava.m@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'C'), +('cand-12-uuid', '29-0012', 'Lucas Taylor', 'lucas.t@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'B'), +('cand-13-uuid', '29-0013', 'Mia Anderson', 'mia.a@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'B'), +('cand-14-uuid', '29-0014', 'Alexander Thomas', 'alex.t@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'D'), +('cand-15-uuid', '29-0015', 'Charlotte Jackson', 'charlotte.j@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'D') +ON DUPLICATE KEY UPDATE name=VALUES(name); + +INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES +(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-1-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-2-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-3-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-4-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-5-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-6-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-7-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-8-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-9-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-10-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-11-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-12-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-13-uuid', 'Uswag', 1), +(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-14-uuid', 'Maligaya', 1), +(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-15-uuid', 'Uswag', 1); diff --git a/db/migrations/007_officer_management_data.sql b/db/migrations/007_officer_management_data.sql new file mode 100644 index 0000000..7afffbb --- /dev/null +++ b/db/migrations/007_officer_management_data.sql @@ -0,0 +1,6 @@ +-- Add Sample Officers for Management View +INSERT INTO users (id, student_id, name, email, password_hash, role, access_level) +VALUES +('officer-uuid-1', '23-5443', 'Jay Orly Mil Santiago', 'jay44296@gmail.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Adviser', 3), +('officer-uuid-2', '23-1111', 'Ma. Elena Santos', 'elena.santos@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Officer', 2), +('officer-uuid-3', '23-2222', 'Robert Chen', 'robert.chen@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Officer', 2); diff --git a/db/migrations/008_audit_trail_data.sql b/db/migrations/008_audit_trail_data.sql new file mode 100644 index 0000000..b5aa5bf --- /dev/null +++ b/db/migrations/008_audit_trail_data.sql @@ -0,0 +1,23 @@ +-- Add details column to audit_logs for human-readable descriptions +ALTER TABLE audit_logs ADD COLUMN details TEXT AFTER action; + +-- Clear existing logs for a fresh start in development +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE audit_logs; +SET FOREIGN_KEY_CHECKS = 1; + +-- Insert sample audit data matching the UI design +INSERT INTO audit_logs (id, user_id, action, details, created_at) VALUES +('a1', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-15 18:22:30'), +('a2', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 04:09:53'), +('a3', 'admin-uuid-1', 'Update Election Status', 'Changed SY 2028 Election status from Preparing to Ongoing', '2026-02-11 04:09:43'), +('a4', 'admin-uuid-1', 'Remove Position', 'Removed position: Position_Name', '2026-02-11 04:08:34'), +('a5', 'admin-uuid-1', 'Add Position', 'Added new position: Position_Name (Uniform)', '2026-02-11 04:08:30'), +('a6', 'admin-uuid-1', 'Add Voter', 'Registered new voter: jay44296@gmail.com for Election ID: 6. User ID: 12-3456', '2026-02-11 04:07:57'), +('a7', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-11 04:03:00'), +('a8', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 04:02:25'), +('a9', 'admin-uuid-1', 'Delete Voter', 'Deleted voter: jay44296@gmail.com (ID: 12-3456)', '2026-02-11 03:58:30'), +('a10', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-11 03:53:37'), +('a11', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 03:49:56'), +('a12', 'admin-uuid-1', 'Add Candidate', 'Added candidate Vanessa Ortega for Board Member. ID: 25-7916', '2026-02-11 03:49:30'), +('a13', 'admin-uuid-1', 'Add Candidate', 'Added candidate Noah Padilla for Board Member. ID: 77-4683', '2026-02-11 03:49:10'); diff --git a/db/migrations/009_multi_election_support.sql b/db/migrations/009_multi_election_support.sql new file mode 100644 index 0000000..a9d7063 --- /dev/null +++ b/db/migrations/009_multi_election_support.sql @@ -0,0 +1,32 @@ +-- Migration to support multi-election and enhanced candidate management +SET FOREIGN_KEY_CHECKS = 0; + +-- Create parties table for definition +CREATE TABLE IF NOT EXISTS parties ( + id CHAR(36) PRIMARY KEY, + election_id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + logo_url VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE +); + +-- Add election_id to audit_logs if not exists +-- Check if column exists is not directly possible in standard SQL without procedural, but we can try to add it. +-- Since this is a fresh migration for polishing, we assume it's okay. +ALTER TABLE audit_logs ADD COLUMN election_id CHAR(36) NULL; +ALTER TABLE audit_logs ADD CONSTRAINT fk_audit_election FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE; + +-- Ensure election_assignments is used correctly +-- We don't need to change the schema here, but we will update the logic. + +-- Add some sample parties for the existing elections +INSERT INTO parties (id, election_id, name, description) +SELECT UUID(), id, 'PROGRESSIVE PARTY', 'Committed to innovation and change.' FROM elections; +INSERT INTO parties (id, election_id, name, description) +SELECT UUID(), id, 'UNITY ALLIANCE', 'Together for a better future.' FROM elections; +INSERT INTO parties (id, election_id, name, description) +SELECT UUID(), id, 'YOUTH VOICE', 'Empowering the next generation.' FROM elections; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/election_history.php b/election_history.php new file mode 100644 index 0000000..7f9428f --- /dev/null +++ b/election_history.php @@ -0,0 +1,336 @@ +query("SELECT * FROM elections WHERE archived = FALSE ORDER BY start_date_and_time DESC")->fetchAll(); + +// Extract years for the "Jump to School Year" dropdown +$years = []; +foreach ($elections as $e) { + $year = date('Y', strtotime($e['start_date_and_time'])); + $years[$year] = $year; +} +krsort($years); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Election History | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+

Election History

+

Voter turnout and candidate results per election year

+
+ +
+ +
+ prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?"); + $voterCount->execute([$election['id']]); + $totalVoters = $voterCount->fetchColumn(); + + $results = $pdo->prepare(" + SELECT c.*, u.name as candidate_name, p.name as position_name, + (SELECT COUNT(*) FROM votes v WHERE v.candidate_id = c.id) as vote_count + FROM candidates c + JOIN users u ON c.user_id = u.id + JOIN positions p ON c.position_id = p.id + WHERE c.election_id = ? + ORDER BY p.sort_order, vote_count DESC + "); + $results->execute([$election['id']]); + $candidates = $results->fetchAll(); + ?> +
+
+
+
+ + + + +
+
+
+
+
+
Total Voters
+
+
+
+
Election Period
+
+ to + +
+
+
+ +
+
Candidate Results
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Candidate NamePositionPartyVotes
+ No candidate results available for this election. +
+
+
+
+ +
+
+
+ + + + diff --git a/includes/sidebar.php b/includes/sidebar.php new file mode 100644 index 0000000..9b628c4 --- /dev/null +++ b/includes/sidebar.php @@ -0,0 +1,60 @@ + + diff --git a/index.php b/index.php index 77ca2cd..b509f02 100644 --- a/index.php +++ b/index.php @@ -7,6 +7,11 @@ if (!$user) { exit; } +if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) { + include 'dashboard.php'; + exit; +} + $pdo = db(); $elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll(); diff --git a/migrate.php b/migrate.php index e882fd3..cf63fd0 100644 --- a/migrate.php +++ b/migrate.php @@ -3,18 +3,21 @@ require_once __DIR__ . '/db/config.php'; try { $pdo = db(); - $sql = file_get_contents(__DIR__ . '/db/migrations/001_initial_schema.sql'); + $migrationFiles = glob(__DIR__ . '/db/migrations/*.sql'); + sort($migrationFiles); - // Split SQL by semicolon and execute each statement - // Note: This is a simple parser, might fail on complex SQL but should work for this schema - $statements = explode(';', $sql); - foreach ($statements as $statement) { - $statement = trim($statement); - if ($statement) { - $pdo->exec($statement); + foreach ($migrationFiles as $file) { + $sql = file_get_contents($file); + $statements = explode(';', $sql); + foreach ($statements as $statement) { + $statement = trim($statement); + if ($statement) { + $pdo->exec($statement); + } } + echo "Executed: " . basename($file) . "\n"; } - echo "Migration successful!\n"; + echo "All migrations completed successfully!\n"; } catch (Exception $e) { echo "Migration failed: " . $e->getMessage() . "\n"; } diff --git a/officers_management.php b/officers_management.php new file mode 100644 index 0000000..87db9cf --- /dev/null +++ b/officers_management.php @@ -0,0 +1,211 @@ +prepare($query . " AND u.role = 'Admin' ORDER BY u.name"); +$stmt->execute([$electionId]); +$admins = $stmt->fetchAll(); + +$stmt = $pdo->prepare($query . " AND u.role = 'Adviser' ORDER BY u.name"); +$stmt->execute([$electionId]); +$advisers = $stmt->fetchAll(); + +$stmt = $pdo->prepare($query . " AND u.role = 'Officer' ORDER BY u.name"); +$stmt->execute([$electionId]); +$officers = $stmt->fetchAll(); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Officer Management | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+
+ +
+
+

Officer Management

+

Personnel for

+
+
+
+ + +
+
+ + Assign New Officer to Election +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+
+
+ + Admins +
+ ACTIVE +
+
+ +
+
+
+
+ + | +
+
+
+ +
+
+ + +
No admins assigned.
+ +
+
+ + +
+
+
+ + Advisers +
+ ACTIVE +
+
+ +
+
+
+
+ + | +
+
+
+ +
+
+ + +
No advisers assigned.
+ +
+
+ + +
+
+
+ + COMEA Officers +
+ ACTIVE +
+
+ +
+
+
+
+ + | +
+
+
+ +
+
+ + +
No officers assigned.
+ +
+
+
+
+
+ + + + diff --git a/reports_audit.php b/reports_audit.php new file mode 100644 index 0000000..664f7ab --- /dev/null +++ b/reports_audit.php @@ -0,0 +1,150 @@ +prepare($query); +$stmt->execute($params); +$logs = $stmt->fetchAll(); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Reports & Audit | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+
+ +
+
+

Reports & Audit Trail

+

Monitoring activity for

+
+
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
TIMESTAMPUSERACTIONDETAILS
No activity logs found for this election.
+
+
+ +
+
+
+
()
+
+
+
+ + + +
+
+
+
+ + + + diff --git a/voter_management.php b/voter_management.php new file mode 100644 index 0000000..68f98aa --- /dev/null +++ b/voter_management.php @@ -0,0 +1,254 @@ +prepare("SELECT COUNT(*) FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter'"); +$totalVoters->execute([$electionId]); +$totalVoters = $totalVoters->fetchColumn(); + +$votedCount = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?"); +$votedCount->execute([$electionId]); +$votedCount = $votedCount->fetchColumn(); + +$notVotedCount = $totalVoters - $votedCount; + +// Distribution (Filtered by Election) +$trackStats = $pdo->prepare("SELECT u.track, COUNT(*) as count FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter' GROUP BY u.track ORDER BY u.track"); +$trackStats->execute([$electionId]); +$trackStats = $trackStats->fetchAll(PDO::FETCH_ASSOC); + +$gradeStats = $pdo->prepare("SELECT u.grade_level, COUNT(*) as count FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter' GROUP BY u.grade_level ORDER BY u.grade_level"); +$gradeStats->execute([$electionId]); +$gradeStats = $gradeStats->fetchAll(PDO::FETCH_ASSOC); + +// Filters +$search = $_GET['search'] ?? ''; +$filterTrack = $_GET['track'] ?? 'All Tracks'; +$filterGrade = $_GET['grade'] ?? 'All Grades'; + +// Query Construction +$query = "SELECT u.*, + (SELECT COUNT(*) FROM votes v WHERE v.voter_id = u.id AND v.election_id = ?) as has_voted + FROM users u + JOIN election_assignments ea ON u.id = ea.user_id + WHERE ea.election_id = ? AND ea.role_in_election = 'Voter'"; + +$params = [$electionId, $electionId]; + +if ($search) { + $query .= " AND (u.email LIKE ? OR u.name LIKE ? OR u.student_id LIKE ?)"; + $params[] = "%$search%"; + $params[] = "%$search%"; + $params[] = "%$search%"; +} + +if ($filterTrack !== 'All Tracks') { + $query .= " AND u.track = ?"; + $params[] = $filterTrack; +} + +if ($filterGrade !== 'All Grades') { + $query .= " AND u.grade_level = ?"; + $params[] = $filterGrade; +} + +$stmt = $pdo->prepare($query); +$stmt->execute($params); +$voters = $stmt->fetchAll(); + +// Get unique values for filters +$tracks = $pdo->query("SELECT DISTINCT track FROM users WHERE track IS NOT NULL ORDER BY track")->fetchAll(PDO::FETCH_COLUMN); +$grades = $pdo->query("SELECT DISTINCT grade_level FROM users WHERE grade_level IS NOT NULL ORDER BY grade_level")->fetchAll(PDO::FETCH_COLUMN); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School'; +?> + + + + + + Voter Management | <?= htmlspecialchars($projectDescription) ?> + + + + + + + + + + + + +
+
+ + + +
+ +
+
+
+
+ +
+
+

Voters List

+

Managing voters for

+
+
+
+ + +
+
+
TOTAL VOTERS
+
+
+
+
VOTERS WHO VOTED
+
+
+
+
VOTERS WHO HAVEN'T VOTED
+
+
+
+ +
+ + +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
USER IDNAMEEMAILTRACKGRADESTATUSACTIONS
No voters assigned to this election.
Grade + + + + + + +
+
+
+
+ + + + + + +