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