Autosave: 20260215-194947
This commit is contained in:
parent
82e2096ac9
commit
ed25faf216
@ -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
29
api/add_party.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -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
19
assets/css/animations.css
Normal 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; }
|
||||
251
assets/css/candidate_management.css
Normal file
251
assets/css/candidate_management.css
Normal 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
394
assets/css/dashboard.css
Normal 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);
|
||||
}
|
||||
195
assets/css/officers_management.css
Normal file
195
assets/css/officers_management.css
Normal 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;
|
||||
}
|
||||
119
assets/css/reports_audit.css
Normal file
119
assets/css/reports_audit.css
Normal 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);
|
||||
}
|
||||
338
assets/css/voter_management.css
Normal file
338
assets/css/voter_management.css
Normal 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;
|
||||
}
|
||||
BIN
assets/pasted-20260215-190109-c933c977.png
Normal file
BIN
assets/pasted-20260215-190109-c933c977.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/pasted-20260215-191054-6f35b633.png
Normal file
BIN
assets/pasted-20260215-191054-6f35b633.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
BIN
assets/pasted-20260215-191356-5299f94b.png
Normal file
BIN
assets/pasted-20260215-191356-5299f94b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
assets/pasted-20260215-192057-e6f6fe5d.png
Normal file
BIN
assets/pasted-20260215-192057-e6f6fe5d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
assets/pasted-20260215-192750-25aa33d0.png
Normal file
BIN
assets/pasted-20260215-192750-25aa33d0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/pasted-20260215-193006-1ef97853.png
Normal file
BIN
assets/pasted-20260215-193006-1ef97853.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@ -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
475
candidate_management.php
Normal 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
260
dashboard.php
Normal 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>
|
||||
@ -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);
|
||||
|
||||
47
db/migrations/002_sample_data.sql
Normal file
47
db/migrations/002_sample_data.sql
Normal 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);
|
||||
39
db/migrations/003_chart_data.sql
Normal file
39
db/migrations/003_chart_data.sql
Normal 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');
|
||||
22
db/migrations/004_election_history_data.sql
Normal file
22
db/migrations/004_election_history_data.sql
Normal 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');
|
||||
10
db/migrations/005_voter_management_data.sql
Normal file
10
db/migrations/005_voter_management_data.sql
Normal 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');
|
||||
49
db/migrations/006_candidate_management_data.sql
Normal file
49
db/migrations/006_candidate_management_data.sql
Normal 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);
|
||||
6
db/migrations/007_officer_management_data.sql
Normal file
6
db/migrations/007_officer_management_data.sql
Normal 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);
|
||||
23
db/migrations/008_audit_trail_data.sql
Normal file
23
db/migrations/008_audit_trail_data.sql
Normal 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');
|
||||
32
db/migrations/009_multi_election_support.sql
Normal file
32
db/migrations/009_multi_election_support.sql
Normal 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
336
election_history.php
Normal 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
60
includes/sidebar.php
Normal 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>
|
||||
@ -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();
|
||||
|
||||
|
||||
21
migrate.php
21
migrate.php
@ -3,18 +3,21 @@ require_once __DIR__ . '/db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = file_get_contents(__DIR__ . '/db/migrations/001_initial_schema.sql');
|
||||
$migrationFiles = glob(__DIR__ . '/db/migrations/*.sql');
|
||||
sort($migrationFiles);
|
||||
|
||||
// Split SQL by semicolon and execute each statement
|
||||
// Note: This is a simple parser, might fail on complex SQL but should work for this schema
|
||||
$statements = explode(';', $sql);
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
if ($statement) {
|
||||
$pdo->exec($statement);
|
||||
foreach ($migrationFiles as $file) {
|
||||
$sql = file_get_contents($file);
|
||||
$statements = explode(';', $sql);
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
if ($statement) {
|
||||
$pdo->exec($statement);
|
||||
}
|
||||
}
|
||||
echo "Executed: " . basename($file) . "\n";
|
||||
}
|
||||
echo "Migration successful!\n";
|
||||
echo "All migrations completed successfully!\n";
|
||||
} catch (Exception $e) {
|
||||
echo "Migration failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
211
officers_management.php
Normal file
211
officers_management.php
Normal 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
150
reports_audit.php
Normal 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
254
voter_management.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user