Autosave: 20260215-194947

This commit is contained in:
Flatlogic Bot 2026-02-15 19:49:48 +00:00
parent 82e2096ac9
commit ed25faf216
34 changed files with 3373 additions and 15 deletions

View File

@ -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());

29
api/add_party.php Normal file
View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$election_id = $_POST['election_id'] ?? '';
$name = $_POST['name'] ?? '';
$description = $_POST['description'] ?? '';
if (!$election_id || !$name) {
die("Missing fields");
}
try {
$pdo = db();
$id = uuid();
$stmt = $pdo->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());
}
}

View File

@ -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());

19
assets/css/animations.css Normal file
View File

@ -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; }

View File

@ -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%;
}

394
assets/css/dashboard.css Normal file
View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -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();
}

475
candidate_management.php Normal file
View File

@ -0,0 +1,475 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
// Global Election Context
$electionId = get_active_election_id();
$election = get_active_election();
if (!$electionId) {
die("No active election selected. Please create an election first.");
}
// Statistics (Filtered by Election)
$totalCandidates = $pdo->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';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Candidate Management | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<link rel="stylesheet" href="assets/css/candidate_management.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.management-actions {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.btn-manage {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e2e8f0;
background: white;
color: #1e293b;
}
.btn-manage:hover {
background: #f8fafc;
border-color: #cbd5e1;
}
.btn-manage.primary {
background: #4f46e5;
color: white;
border-color: #4f46e5;
}
.btn-manage.primary:hover {
background: #4338ca;
}
.modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 32px;
border-radius: 16px;
width: 100%;
max-width: 500px;
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 { margin: 0; font-size: 1.25rem; color: #1e293b; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 1px solid #e2e8f0;
font-size: 14px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
</style>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Quick search...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="dashboard-header" style="display: flex; justify-content: space-between; align-items: flex-start;">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="header-icon-container">
<i data-lucide="user-square-2" style="width: 24px; color: #4f46e5;"></i>
</div>
<div>
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Candidate Management</h1>
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Managing <?= htmlspecialchars($election['title']) ?></p>
</div>
</div>
<div>
<span class="status-badge status-<?= strtolower($election['status'] ?? 'preparing') ?>">
<?= strtoupper($election['status'] ?? 'PREPARING') ?>
</span>
</div>
</div>
<div class="management-actions animate-stagger">
<button class="btn-manage primary" onclick="openModal('addCandidateModal')">
<i data-lucide="plus"></i> ADD CANDIDATE
</button>
<button class="btn-manage" onclick="openModal('addPositionModal')">
<i data-lucide="layout-list"></i> DEFINE POSITION
</button>
<button class="btn-manage" onclick="openModal('addPartyModal')">
<i data-lucide="flag"></i> DEFINE PARTY
</button>
</div>
<!-- Stats Grid -->
<div class="candidate-stats-grid animate-stagger">
<div class="candidate-stat-card">
<div class="candidate-stat-label">TOTAL CANDIDATES</div>
<div class="candidate-stat-value"><?= number_format($totalCandidates) ?></div>
</div>
<div class="candidate-stat-card">
<div class="candidate-stat-label">UNIQUE POSITIONS</div>
<div class="candidate-stat-value"><?= number_format($uniquePositions) ?></div>
</div>
<div class="candidate-stat-card">
<div class="candidate-stat-label" style="color: #10b981;">ACTIVE PARTIES</div>
<div class="candidate-stat-value" style="color: #10b981;"><?= number_format($activeParties) ?></div>
</div>
</div>
<!-- Distribution Row -->
<div class="distribution-row animate-stagger" style="margin-bottom: 32px;">
<div class="distribution-card">
<div class="distribution-header">Candidates by Position</div>
<div class="distribution-list">
<?php foreach ($posStats as $stat): ?>
<div class="distribution-item">
<span><?= htmlspecialchars($stat['name']) ?></span>
<span class="distribution-count"><?= $stat['count'] ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($posStats)): ?>
<div style="padding: 12px; color: #94a3b8; font-size: 0.875rem; text-align: center;">No positions defined.</div>
<?php endif; ?>
</div>
</div>
<div class="distribution-card">
<div class="distribution-header">Candidates by Party</div>
<div class="distribution-list">
<?php foreach ($partyStats as $stat): ?>
<div class="distribution-item">
<span><?= htmlspecialchars($stat['party_name'] ?: 'Independent') ?></span>
<span class="distribution-count"><?= $stat['count'] ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($partyStats)): ?>
<div style="padding: 12px; color: #94a3b8; font-size: 0.875rem; text-align: center;">No parties defined.</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Filters & Table Section -->
<div class="content-section animate-fade-in" style="background: white; border-radius: 12px; border: 1px solid #f3f4f6; overflow: hidden;">
<form method="GET" class="filter-bar">
<div class="filter-group" style="flex: 2;">
<label>SEARCH</label>
<div class="search-input-wrapper">
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search candidates...">
</div>
</div>
<div class="filter-group">
<label>POSITION</label>
<select name="position" onchange="this.form.submit()">
<option>All Positions</option>
<?php foreach ($allPositions as $p): ?>
<option value="<?= htmlspecialchars($p['name']) ?>" <?= $filterPosition === $p['name'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label>PARTY</label>
<select name="party" onchange="this.form.submit()">
<option>All Parties</option>
<?php foreach ($allParties as $pt): ?>
<option value="<?= htmlspecialchars($pt['name']) ?>" <?= $filterParty === $pt['name'] ? 'selected' : '' ?>><?= htmlspecialchars($pt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
<table class="candidates-table">
<thead>
<tr>
<th>CANDIDATE</th>
<th>POSITION</th>
<th>PARTY</th>
<th>GRADE/TRACK</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<?php if (empty($candidates)): ?>
<tr>
<td colspan="5" style="text-align: center; color: #94a3b8; padding: 32px;">No candidates found in this election.</td>
</tr>
<?php else: ?>
<?php foreach ($candidates as $cand): ?>
<tr>
<td>
<div class="candidate-info">
<div class="candidate-avatar">
<?= strtoupper(substr($cand['user_name'], 0, 1)) ?>
</div>
<div class="candidate-details">
<span class="candidate-name"><?= htmlspecialchars($cand['user_name']) ?></span>
<span class="candidate-sub"><?= htmlspecialchars($cand['student_id']) ?> | <?= htmlspecialchars($cand['user_email']) ?></span>
</div>
</div>
</td>
<td>
<span class="position-badge"><?= htmlspecialchars($cand['position_name']) ?></span>
</td>
<td><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></td>
<td>
<div class="candidate-details">
<span class="candidate-name">Grade <?= htmlspecialchars($cand['grade_level'] ?: '12') ?></span>
<span class="candidate-sub"><?= htmlspecialchars($cand['track'] ?: 'N/A') ?></span>
</div>
</td>
<td class="actions-cell">
<button title="Edit"><i data-lucide="edit-2"></i></button>
<button title="Delete" style="color: #ef4444;"><i data-lucide="trash-2"></i></button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
</div>
<!-- Modals -->
<div id="addCandidateModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Add New Candidate</h2>
<button onclick="closeModal('addCandidateModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
</div>
<form action="api/add_candidate.php" method="POST">
<input type="hidden" name="election_id" value="<?= $electionId ?>">
<div class="form-group">
<label>Select Student</label>
<select name="user_id" required>
<option value="">-- Choose Voter --</option>
<?php foreach ($allVoters as $v): ?>
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?> (<?= $v['student_id'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Position</label>
<select name="position_id" required>
<option value="">-- Choose Position --</option>
<?php foreach ($allPositions as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Party</label>
<select name="party_name">
<option value="">Independent</option>
<?php foreach ($allParties as $pt): ?>
<option value="<?= htmlspecialchars($pt['name']) ?>"><?= htmlspecialchars($pt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Manifesto / Vision</label>
<textarea name="manifesto" rows="3" placeholder="Enter candidate vision..."></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="closeModal('addCandidateModal')" class="btn-manage">Cancel</button>
<button type="submit" class="btn-manage primary">Save Candidate</button>
</div>
</form>
</div>
</div>
<div id="addPositionModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Define New Position</h2>
<button onclick="closeModal('addPositionModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
</div>
<form action="api/add_position.php" method="POST">
<input type="hidden" name="election_id" value="<?= $electionId ?>">
<div class="form-group">
<label>Position Name</label>
<input type="text" name="name" placeholder="e.g. President, Secretary" required>
</div>
<div class="form-group">
<label>Max Votes (Winners)</label>
<input type="number" name="max_votes" value="1" min="1" required>
</div>
<div class="modal-footer">
<button type="button" onclick="closeModal('addPositionModal')" class="btn-manage">Cancel</button>
<button type="submit" class="btn-manage primary">Create Position</button>
</div>
</form>
</div>
</div>
<div id="addPartyModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Define New Party</h2>
<button onclick="closeModal('addPartyModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
</div>
<form action="api/add_party.php" method="POST">
<input type="hidden" name="election_id" value="<?= $electionId ?>">
<div class="form-group">
<label>Party Name</label>
<input type="text" name="name" placeholder="e.g. Unity Party" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" rows="2" placeholder="Party slogan or vision..."></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="closeModal('addPartyModal')" class="btn-manage">Cancel</button>
<button type="submit" class="btn-manage primary">Create Party</button>
</div>
</form>
</div>
</div>
<script>
lucide.createIcons();
function openModal(id) {
document.getElementById(id).style.display = 'flex';
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
window.onclick = function(event) {
if (event.target.className === 'modal') {
event.target.style.display = 'none';
}
}
</script>
</body>
</html>

260
dashboard.php Normal file
View File

@ -0,0 +1,260 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
$electionId = get_active_election_id();
$election = get_active_election();
// Statistics (Filtered by Election)
$totalVoters = $pdo->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';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Election Dashboard | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Search for voters, candidates, or records...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="dashboard-header">
<div>
<h1 style="margin: 0 0 4px 0; font-size: 1.5rem; color: #1e293b;">Election Dashboard</h1>
<div class="welcome-msg">
Active Election: <strong><?= htmlspecialchars($election['title'] ?? 'None') ?></strong>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid animate-stagger">
<div class="stat-card">
<div class="stat-label">Total Voters</div>
<div class="stat-value"><?= number_format($totalVoters) ?></div>
<div class="stat-footer voters">
<i data-lucide="users-2" style="width: 14px;"></i>
Assigned Students
</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Candidates</div>
<div class="stat-value"><?= number_format($totalCandidates) ?></div>
<div class="stat-footer candidates">
<i data-lucide="user-circle" style="width: 14px;"></i>
Validated Contestants
</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Votes Cast</div>
<div class="stat-value"><?= number_format($totalVotes) ?></div>
<div class="stat-footer votes">
<i data-lucide="check-circle-2" style="width: 14px;"></i>
Verified Ballots
</div>
</div>
</div>
<!-- Analytics Charts -->
<div class="analytics-row animate-stagger">
<div class="analytics-card">
<div class="card-header">
<div class="card-title">Votes Per Grade Level</div>
</div>
<div class="chart-container">
<canvas id="gradeChart"></canvas>
</div>
</div>
<div class="analytics-card">
<div class="card-header">
<div class="card-title">Votes Per Track</div>
</div>
<div class="chart-container">
<canvas id="trackChart"></canvas>
</div>
</div>
</div>
<div class="analytics-row animate-stagger">
<div class="analytics-card" style="grid-column: span 2;">
<div class="card-header">
<div class="card-title">Votes Per Section</div>
<select id="trackFilter" class="chart-filter">
<?php if (empty($tracks)): ?>
<option>No data</option>
<?php endif; ?>
<?php foreach ($tracks as $t): ?>
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="chart-container" style="height: 300px;">
<canvas id="sectionChart"></canvas>
</div>
</div>
</div>
</main>
</div>
<script>
lucide.createIcons();
// Chart Data from PHP
const gradeData = <?= json_encode($gradeStats) ?>;
const trackData = <?= json_encode($trackStats) ?>;
const sectionData = <?= json_encode($sectionStats) ?>;
// Common Chart Options
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { display: false } },
x: { grid: { display: false } }
}
};
// Grade Level Bar Chart
if (gradeData.length) {
new Chart(document.getElementById('gradeChart'), {
type: 'bar',
data: {
labels: gradeData.map(d => 'Grade ' + d.label),
datasets: [{
label: 'Votes',
data: gradeData.map(d => d.count),
backgroundColor: '#4f46e5',
borderRadius: 6
}]
},
options: commonOptions
});
}
// Track Bar Chart
if (trackData.length) {
new Chart(document.getElementById('trackChart'), {
type: 'bar',
data: {
labels: trackData.map(d => d.label),
datasets: [{
label: 'Votes',
data: trackData.map(d => d.count),
backgroundColor: '#10b981',
borderRadius: 6
}]
},
options: commonOptions
});
}
// Section Chart
let sectionChart;
function updateSectionChart(track) {
const canvas = document.getElementById('sectionChart');
if (!canvas) return;
const filtered = sectionData.filter(d => d.track === track);
const data = {
labels: filtered.map(d => d.label),
datasets: [{
label: 'Votes',
data: filtered.map(d => d.count),
backgroundColor: '#4f46e5',
borderRadius: 6
}]
};
if (sectionChart) {
sectionChart.data = data;
sectionChart.update();
} else {
sectionChart = new Chart(canvas, {
type: 'bar',
data: data,
options: commonOptions
});
}
}
const trackFilter = document.getElementById('trackFilter');
if (trackFilter && trackData.length) {
trackFilter.addEventListener('change', (e) => {
updateSectionChart(e.target.value);
});
updateSectionChart(trackFilter.value);
}
</script>
</body>
</html>

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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');

View File

@ -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');

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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;

336
election_history.php Normal file
View File

@ -0,0 +1,336 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
// Fetch all elections for the history
$elections = $pdo->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';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Election History | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.history-controls {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.history-title-area h1 {
margin: 0 0 4px 0;
font-size: 1.5rem;
color: #1e293b;
}
.history-title-area p {
margin: 0;
color: #64748b;
font-size: 0.875rem;
}
.year-selector {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: white;
color: #4b5563;
font-size: 0.875rem;
outline: none;
}
.election-history-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.election-item {
background: white;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
}
.election-item-header {
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.election-item-header:hover {
background: #f8fafc;
}
.election-item-title {
font-weight: 600;
color: #2563eb;
font-size: 1rem;
}
.election-item-right {
display: flex;
align-items: center;
gap: 16px;
}
.status-badge {
padding: 4px 12px;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.status-ongoing { background: #fffbeb; color: #d97706; }
.status-finished { background: #ecfdf5; color: #10b981; }
.election-item-body {
display: none;
padding: 24px;
border-top: 1px solid var(--border-color);
background: #fafafa;
}
.election-item.active .election-item-body {
display: block;
}
.election-item.active .chevron-icon {
transform: rotate(180deg);
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 24px;
}
.detail-card {
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
}
.detail-label {
font-size: 0.7rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
margin-bottom: 12px;
}
.detail-value {
font-size: 1.5rem;
font-weight: 700;
color: #1e293b;
}
.detail-sub {
font-size: 0.875rem;
color: #1e293b;
font-weight: 500;
}
.results-section {
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.results-title {
padding: 16px 20px;
font-weight: 700;
font-size: 0.9rem;
color: #1e293b;
border-bottom: 1px solid var(--border-color);
}
.results-table {
width: 100%;
border-collapse: collapse;
}
.results-table th {
background: #f9fafb;
padding: 10px 20px;
text-align: left;
font-size: 0.7rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
}
.results-table td {
padding: 12px 20px;
border-bottom: 1px solid var(--border-color);
font-size: 0.875rem;
color: #4b5563;
}
</style>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Search for records...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="history-controls">
<div class="history-title-area">
<h1>Election History</h1>
<p>Voter turnout and candidate results per election year</p>
</div>
<select class="year-selector" onchange="jumpToYear(this.value)">
<option value="">Jump to School Year</option>
<?php foreach ($years as $y): ?>
<option value="year-<?= $y ?>">SY <?= $y ?>-<?= $y+1 ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="election-history-list animate-stagger">
<?php foreach ($elections as $election):
$electionYear = date('Y', strtotime($election['start_date_and_time']));
$voterCount = $pdo->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();
?>
<div class="election-item" id="year-<?= $electionYear ?>">
<div class="election-item-header" onclick="toggleAccordion(this)">
<div class="election-item-title"><?= htmlspecialchars($election['title']) ?></div>
<div class="election-item-right">
<span class="status-badge status-<?= strtolower($election['status']) ?>">
<?= htmlspecialchars($election['status']) ?>
</span>
<i data-lucide="chevron-down" class="chevron-icon" style="width: 18px; color: #2563eb;"></i>
</div>
</div>
<div class="election-item-body">
<div class="details-grid">
<div class="detail-card">
<div class="detail-label">Total Voters</div>
<div class="detail-value"><?= number_format($totalVoters) ?></div>
</div>
<div class="detail-card">
<div class="detail-label">Election Period</div>
<div class="detail-sub">
<?= date('M d, Y', strtotime($election['start_date_and_time'])) ?> to
<?= date('M d, Y', strtotime($election['end_date_and_time'])) ?>
</div>
</div>
</div>
<div class="results-section">
<div class="results-title">Candidate Results</div>
<table class="results-table">
<thead>
<tr>
<th>Candidate Name</th>
<th>Position</th>
<th>Party</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
<?php if (empty($candidates)): ?>
<tr>
<td colspan="4" style="text-align: center; padding: 20px; color: #94a3b8;">
No candidate results available for this election.
</td>
</tr>
<?php else: ?>
<?php foreach ($candidates as $cand): ?>
<tr>
<td style="font-weight: 500; color: #1e293b;"><?= htmlspecialchars($cand['candidate_name']) ?></td>
<td><?= htmlspecialchars($cand['position_name']) ?></td>
<td><?= htmlspecialchars($cand['party_name'] ?? 'Independent') ?></td>
<td style="font-weight: 600; color: #2563eb;"><?= number_format($cand['vote_count']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</main>
</div>
<script>
lucide.createIcons();
function toggleAccordion(header) {
const item = header.parentElement;
const isActive = item.classList.contains('active');
if (isActive) {
item.classList.remove('active');
} else {
item.classList.add('active');
}
}
function jumpToYear(id) {
if (!id) return;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
el.classList.add('active');
}
}
</script>
</body>
</html>

60
includes/sidebar.php Normal file
View File

@ -0,0 +1,60 @@
<?php
$activeElectionId = get_active_election_id();
$allElections = get_all_elections();
$currentElection = get_active_election();
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<aside class="sidebar">
<div class="sidebar-header">
<div class="sidebar-brand">CLICK TO VOTE</div>
<div class="sidebar-subtitle">Administrator Portal</div>
</div>
<div class="election-selector-container" style="padding: 0 20px; margin-bottom: 20px;">
<label style="font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; display: block;">ACTIVE ELECTION</label>
<select onchange="window.location.href='?set_election_id=' + this.value" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0; background: #f8fafc; font-size: 12px; font-weight: 500; color: #1e293b; cursor: pointer;">
<?php foreach ($allElections as $e): ?>
<option value="<?= $e['id'] ?>" <?= $activeElectionId === $e['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($e['title']) ?>
</option>
<?php endforeach; ?>
<?php if (empty($allElections)): ?>
<option disabled>No elections found</option>
<?php endif; ?>
</select>
</div>
<nav class="sidebar-nav">
<a href="dashboard.php" class="nav-item <?= $currentPage === 'dashboard.php' ? 'active' : '' ?>">
<i data-lucide="layout-dashboard"></i>
Election Dashboard
</a>
<a href="election_history.php" class="nav-item <?= $currentPage === 'election_history.php' ? 'active' : '' ?>">
<i data-lucide="history"></i>
Election History
</a>
<a href="voter_management.php" class="nav-item <?= $currentPage === 'voter_management.php' ? 'active' : '' ?>">
<i data-lucide="users"></i>
Voter Management
</a>
<a href="candidate_management.php" class="nav-item <?= $currentPage === 'candidate_management.php' ? 'active' : '' ?>">
<i data-lucide="user-square-2"></i>
Candidate Management
</a>
<a href="officers_management.php" class="nav-item <?= $currentPage === 'officers_management.php' ? 'active' : '' ?>">
<i data-lucide="shield-check"></i>
Officers Management
</a>
<a href="reports_audit.php" class="nav-item <?= $currentPage === 'reports_audit.php' ? 'active' : '' ?>">
<i data-lucide="file-text"></i>
Reports & Audit
</a>
</nav>
<div class="sidebar-footer">
<a href="logout.php" class="nav-item" style="color: #ef4444;">
<i data-lucide="log-out"></i>
Logout
</a>
</div>
</aside>

View File

@ -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();

View File

@ -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";
}

211
officers_management.php Normal file
View File

@ -0,0 +1,211 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
$electionId = get_active_election_id();
$election = get_active_election();
// Fetch officers assigned to this election grouped by role
$query = "SELECT u.* FROM users u
JOIN election_assignments ea ON u.id = ea.user_id
WHERE ea.election_id = ? AND u.deleted_at IS NULL";
$stmt = $pdo->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';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Officer Management | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<link rel="stylesheet" href="assets/css/officers_management.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Search officers...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="dashboard-header">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="header-icon-container">
<i data-lucide="shield-check" style="width: 24px; color: #4f46e5;"></i>
</div>
<div>
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Officer Management</h1>
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Personnel for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
</div>
</div>
</div>
<!-- Register New Officer Form -->
<section class="registration-section animate-stagger">
<div class="registration-header">
<i data-lucide="plus-circle" style="width: 20px; color: #2563eb;"></i>
Assign New Officer to Election
</div>
<form class="registration-form">
<div class="form-row">
<div class="form-group">
<label>Full Name</label>
<input type="text" placeholder="Enter name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" placeholder="email@school.edu">
</div>
<div class="form-group">
<label>Role</label>
<select>
<option value="Admin">Admin</option>
<option value="Adviser">Adviser</option>
<option value="Officer">Officer</option>
</select>
</div>
</div>
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
<button type="button" class="btn-save-officer">
<i data-lucide="save" style="width: 18px;"></i>
ASSIGN TO ELECTION
</button>
</div>
</form>
</section>
<!-- Officer Categories Grid -->
<div class="officer-management-grid animate-fade-in">
<!-- Admins -->
<div class="officer-category-card">
<div class="category-header">
<div class="category-title">
<i data-lucide="user-cog" style="width: 18px; color: #4f46e5;"></i>
Admins
</div>
<span class="active-count"><?= count($admins) ?> ACTIVE</span>
</div>
<div class="officer-list">
<?php foreach ($admins as $o): ?>
<div class="officer-item">
<div class="officer-main-info">
<div class="officer-avatar"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
<div class="officer-details">
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
</div>
</div>
<div class="officer-actions">
<button title="Edit"><i data-lucide="edit-3" style="width: 16px;"></i></button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($admins)): ?>
<div class="empty-state">No admins assigned.</div>
<?php endif; ?>
</div>
</div>
<!-- COMEA Advisers -->
<div class="officer-category-card">
<div class="category-header">
<div class="category-title">
<i data-lucide="user-check" style="width: 18px; color: #f97316;"></i>
Advisers
</div>
<span class="active-count" style="background: #fff7ed; color: #ea580c;"><?= count($advisers) ?> ACTIVE</span>
</div>
<div class="officer-list">
<?php foreach ($advisers as $o): ?>
<div class="officer-item">
<div class="officer-main-info">
<div class="officer-avatar" style="background: #eff6ff; color: #2563eb;"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
<div class="officer-details">
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
</div>
</div>
<div class="officer-actions">
<button title="Edit"><i data-lucide="edit-3" style="width: 16px;"></i></button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($advisers)): ?>
<div class="empty-state">No advisers assigned.</div>
<?php endif; ?>
</div>
</div>
<!-- COMEA Officers -->
<div class="officer-category-card">
<div class="category-header">
<div class="category-title">
<i data-lucide="users" style="width: 18px; color: #10b981;"></i>
COMEA Officers
</div>
<span class="active-count" style="background: #f0fdf4; color: #16a34a;"><?= count($officers) ?> ACTIVE</span>
</div>
<div class="officer-list">
<?php foreach ($officers as $o): ?>
<div class="officer-item">
<div class="officer-main-info">
<div class="officer-avatar" style="background: #ecfdf5; color: #059669;"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
<div class="officer-details">
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
</div>
</div>
<div class="officer-actions">
<button title="Edit"><i data-lucide="edit-3" style="width: 16px;"></i></button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($officers)): ?>
<div class="empty-state">No officers assigned.</div>
<?php endif; ?>
</div>
</div>
</div>
</main>
</div>
<script>
lucide.createIcons();
</script>
</body>
</html>

150
reports_audit.php Normal file
View File

@ -0,0 +1,150 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
$electionId = get_active_election_id();
$election = get_active_election();
// Filters
$search = $_GET['search'] ?? '';
// Query Construction
$query = "SELECT l.*, u.student_id, u.role, u.name as user_name
FROM audit_logs l
LEFT JOIN users u ON l.user_id = u.id
WHERE (l.election_id = ? OR l.election_id IS NULL)";
$params = [$electionId];
if ($search) {
$query .= " AND (l.action LIKE ? OR l.details LIKE ? OR u.student_id LIKE ? OR u.name LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
$query .= " ORDER BY l.created_at DESC LIMIT 100";
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$logs = $stmt->fetchAll();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reports & Audit | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<link rel="stylesheet" href="assets/css/reports_audit.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Quick search logs...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="dashboard-header">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="header-icon-container">
<i data-lucide="file-text" style="width: 24px; color: #4f46e5;"></i>
</div>
<div>
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Reports & Audit Trail</h1>
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Monitoring activity for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
</div>
</div>
</div>
<!-- Filters & Table Section -->
<div class="content-section animate-fade-in">
<form method="GET" class="filter-bar">
<div class="filter-group" style="flex: 2;">
<label>SEARCH LOGS</label>
<div class="search-input-wrapper">
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search by action, user, or details">
</div>
</div>
<div style="display: flex; gap: 12px; align-items: flex-end;">
<button type="submit" class="btn-manage primary">Filter</button>
<button type="button" class="btn-manage" onclick="window.print()"><i data-lucide="printer"></i> Print</button>
</div>
</form>
<table class="audit-table">
<thead>
<tr>
<th>TIMESTAMP</th>
<th>USER</th>
<th>ACTION</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr>
<td colspan="4" style="text-align: center; color: #94a3b8; padding: 32px;">No activity logs found for this election.</td>
</tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td style="white-space: nowrap;"><?= date('M d, Y H:i:s', strtotime($log['created_at'])) ?></td>
<td>
<div style="display: flex; align-items: center; gap: 8px;">
<div class="user-avatar-small" style="width: 24px; height: 24px; font-size: 10px; background: #f1f5f9; color: #475569; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600;">
<?= strtoupper(substr($log['user_name'] ?? 'S', 0, 1)) ?>
</div>
<div>
<div style="font-weight: 600; font-size: 13px;"><?= htmlspecialchars($log['user_name'] ?? 'SYSTEM') ?></div>
<div style="font-size: 11px; color: #94a3b8;"><?= htmlspecialchars($log['role'] ?? 'SYSTEM') ?> (<?= htmlspecialchars($log['student_id'] ?? 'N/A') ?>)</div>
</div>
</div>
</td>
<td>
<span class="action-badge"><?= strtoupper(htmlspecialchars($log['action'])) ?></span>
</td>
<td style="color: #64748b; font-size: 13px;">
<?= htmlspecialchars($log['details'] ?? 'No additional details') ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
</div>
<script>
lucide.createIcons();
</script>
</body>
</html>

254
voter_management.php Normal file
View File

@ -0,0 +1,254 @@
<?php
require_once 'auth_helper.php';
require_login();
$user = get_user();
$pdo = db();
$electionId = get_active_election_id();
$election = get_active_election();
// Statistics (Filtered by Election if possible, otherwise global)
// For now, let's assume we want to see voters assigned to the current election
$totalVoters = $pdo->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';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voter Management | <?= htmlspecialchars($projectDescription) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<link rel="stylesheet" href="assets/css/voter_management.css?v=<?= time() ?>">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="dashboard-body">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-wrapper">
<header class="top-header">
<div class="search-bar">
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
<input type="text" placeholder="Quick search...">
</div>
<div class="user-profile">
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
</div>
<div class="user-avatar">
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
</div>
</div>
</header>
<main class="dashboard-content animate-fade-in">
<div class="dashboard-header">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="header-icon-container">
<i data-lucide="users" style="width: 24px; color: #4f46e5;"></i>
</div>
<div>
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Voters List</h1>
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Managing voters for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="voter-stats-grid animate-stagger">
<div class="voter-stat-card">
<div class="voter-stat-label">TOTAL VOTERS</div>
<div class="voter-stat-value" style="color: #2563eb;"><?= number_format($totalVoters) ?></div>
</div>
<div class="voter-stat-card">
<div class="voter-stat-label">VOTERS WHO VOTED</div>
<div class="voter-stat-value" style="color: #64748b;"><?= number_format($votedCount) ?></div>
</div>
<div class="voter-stat-card">
<div class="voter-stat-label" style="color: #ef4444;">VOTERS WHO HAVEN'T VOTED</div>
<div class="voter-stat-value" style="color: #ef4444;"><?= number_format($notVotedCount) ?></div>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 24px;" class="animate-stagger">
<button class="btn-action btn-add" onclick="openModal('addVoterModal')">
<i data-lucide="plus" style="width: 14px;"></i>
ADD VOTER
</button>
<button class="btn-action btn-import" onclick="openModal('importModal')">
<i data-lucide="upload" style="width: 14px;"></i>
Import CSV
</button>
</div>
<!-- Filters & Table Section -->
<div class="content-section animate-fade-in">
<form id="filterForm" method="GET" class="filter-bar">
<div class="filter-group" style="flex: 2;">
<label>SEARCH</label>
<div class="search-input-wrapper">
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search by email">
</div>
</div>
<div class="filter-group">
<label>TRACK</label>
<select name="track" onchange="this.form.submit()">
<option>All Tracks</option>
<?php foreach ($tracks as $t): ?>
<option value="<?= htmlspecialchars($t) ?>" <?= $filterTrack === $t ? 'selected' : '' ?>><?= htmlspecialchars($t) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label>GRADE</label>
<select name="grade" onchange="this.form.submit()">
<option>All Grades</option>
<?php foreach ($grades as $g): ?>
<option value="<?= htmlspecialchars($g) ?>" <?= $filterGrade == $g ? 'selected' : '' ?>>Grade <?= htmlspecialchars($g) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
<table class="voters-table">
<thead>
<tr>
<th>USER ID</th>
<th>NAME</th>
<th>EMAIL</th>
<th>TRACK</th>
<th>GRADE</th>
<th>STATUS</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<?php if (empty($voters)): ?>
<tr>
<td colspan="7" style="text-align: center; color: #94a3b8; padding: 32px;">No voters assigned to this election.</td>
</tr>
<?php else: ?>
<?php foreach ($voters as $voter): ?>
<tr>
<td><?= htmlspecialchars($voter['student_id']) ?></td>
<td><?= htmlspecialchars($voter['name']) ?></td>
<td><?= htmlspecialchars($voter['email']) ?></td>
<td><?= htmlspecialchars($voter['track']) ?></td>
<td>Grade <?= htmlspecialchars($voter['grade_level']) ?></td>
<td>
<span class="status-indicator <?= $voter['has_voted'] ? 'voted' : 'pending' ?>">
<?= $voter['has_voted'] ? 'Voted' : 'Pending' ?>
</span>
</td>
<td class="actions-cell">
<button title="Edit"><i data-lucide="edit-2"></i></button>
<button title="Delete" style="color: #ef4444;"><i data-lucide="trash-2"></i></button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
</div>
<!-- Modals (Simplified for this polishing phase) -->
<div id="addVoterModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Add New Voter</h2>
<button onclick="closeModal('addVoterModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
</div>
<p style="font-size: 0.875rem; color: #64748b;">This will assign an existing user or create a new one for this election.</p>
<!-- Add Voter form logic here -->
<div class="modal-footer">
<button onclick="closeModal('addVoterModal')" class="btn-cancel">Close</button>
</div>
</div>
</div>
<script>
lucide.createIcons();
function openModal(id) {
document.getElementById(id).style.display = 'flex';
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
</script>
</body>
</html>