first commit

This commit is contained in:
2026-05-03 16:55:22 +05:30
commit d3bb4199e1
12081 changed files with 662460 additions and 0 deletions

View File

@@ -0,0 +1,542 @@
<template>
<div class="admin-page">
<div class="admin-container">
<header class="admin-header">
<h1>Admin Control Panel</h1>
<div class="header-actions">
<button @click="fetchData" class="secondary-btn">🔄 Refresh Data</button>
</div>
</header>
<div class="dashboard-layout">
<!-- Left: Forms -->
<section class="config-section">
<!-- Position Management -->
<div class="card">
<h3>Manage Positions</h3>
<form @submit.prevent="addPosition" class="admin-form">
<div class="form-group">
<label>Position Title</label>
<input v-model="newPosition.title" type="text" placeholder="e.g. Head Boy" required />
</div>
<div class="form-group">
<label>Display Order</label>
<input v-model="newPosition.display_order" type="number" placeholder="0" />
</div>
<button type="submit" :disabled="loading" class="primary-btn">
Add Position
</button>
</form>
<div class="positions-list">
<div v-for="pos in positions" :key="pos.id" class="pos-tag">
{{ pos.title }}
</div>
</div>
</div>
<!-- Candidate Registration -->
<div class="card">
<h3>Register New Candidate</h3>
<form @submit.prevent="addCandidate" class="admin-form">
<div class="form-group">
<label>Candidate Name</label>
<input v-model="newCandidate.name" type="text" placeholder="Full Name" required />
</div>
<div class="form-group">
<label>Select Position</label>
<select v-model="newCandidate.position_id" required class="admin-select">
<option disabled value="">Choose position...</option>
<option v-for="pos in positions" :key="pos.id" :value="pos.id">
{{ pos.title }}
</option>
</select>
</div>
<div class="form-group">
<label>Profile Image</label>
<div class="file-input-wrapper">
<input type="file" @change="handleImageUpload" accept="image/*" id="candidate-img" class="hidden-input" required />
<label for="candidate-img" class="file-label">
<span v-if="!newCandidate.image">Choose Image...</span>
<span v-else>{{ newCandidate.image.name }}</span>
</label>
</div>
</div>
<button type="submit" :disabled="loading || positions.length === 0" class="primary-btn">
{{ loading ? 'Saving...' : 'Add Candidate' }}
</button>
<p v-if="positions.length === 0" class="hint">Create a position first!</p>
</form>
</div>
<div class="card danger-card">
<h3>Danger Zone</h3>
<p>Delete all votes and reset the election system. This cannot be undone.</p>
<button @click="showResetModal = true" class="danger-btn">Reset All Votes</button>
</div>
</section>
<!-- Right: Results -->
<section class="results-section">
<div class="card">
<h3>Live Election Results</h3>
<div class="results-container">
<div v-if="groupedResults.length === 0" class="empty-state">
No data available. Register positions and candidates first.
</div>
<div v-for="group in groupedResults" :key="group.position_id" class="position-group">
<h4 class="group-title">{{ group.position_title }}</h4>
<div v-for="candidate in group.candidates" :key="candidate.candidate_id" class="result-row">
<div class="row-header">
<div class="cand-details-admin">
<span class="cand-name">{{ candidate.candidate_name }}</span>
<button @click="deleteCandidate(candidate.candidate_id)" class="delete-btn" title="Remove Candidate">🗑</button>
</div>
<span class="vote-count">{{ candidate.vote_count }} Votes</span>
</div>
<div class="progress-track">
<div
class="progress-fill"
:style="{ width: getPercentage(candidate.vote_count, group.total) + '%' }"
></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Reset Modal -->
<Transition name="fade">
<div v-if="showResetModal" class="modal-backdrop">
<div class="modal reset-modal">
<div class="warning-icon"></div>
<h2>Critical Action Required</h2>
<p>Please complete the following to confirm the full system reset.</p>
<div class="confirm-inputs">
<div class="form-group">
<label>Admin Password</label>
<input v-model="resetConfirm.adminPassword" type="password" />
</div>
<div class="form-group">
<label>Secondary Key (resetvotes2026)</label>
<input v-model="resetConfirm.resetPassword" type="password" />
</div>
<div class="form-group">
<label>Type: "I confirm to reset all votes"</label>
<input v-model="resetConfirm.sentence" type="text" placeholder="Type sentence exactly" />
</div>
</div>
<div class="modal-footer">
<button
class="final-reset-btn"
:disabled="!isResetFormValid"
@click="handleReset"
>
PERFORM SYSTEM RESET
</button>
<button class="cancel-btn" @click="closeResetModal">Cancel</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { candidateService, voteService, positionService } from '../services/api';
const results = ref([]);
const positions = ref([]);
const loading = ref(false);
const showResetModal = ref(false);
const newPosition = reactive({ title: '', display_order: 0 });
const newCandidate = reactive({ name: '', position_id: '', image: null });
const resetConfirm = reactive({ adminPassword: '', resetPassword: '', sentence: '' });
const groupedResults = computed(() => {
const groups = {};
results.value.forEach(r => {
if (!groups[r.position_id]) {
groups[r.position_id] = {
position_id: r.position_id,
position_title: r.position_title,
candidates: [],
total: 0
};
}
groups[r.position_id].candidates.push(r);
groups[r.position_id].total += r.vote_count;
});
return Object.values(groups);
});
const getPercentage = (count, total) => total === 0 ? 0 : (count / total) * 100;
const isResetFormValid = computed(() => {
return resetConfirm.adminPassword === 'adminkcps2026' &&
resetConfirm.resetPassword === 'resetvotes2026' &&
resetConfirm.sentence === 'I confirm to reset all votes';
});
onMounted(fetchData);
async function fetchData() {
await fetchPositions();
await fetchResults();
}
async function fetchPositions() {
try {
const response = await positionService.getAll();
positions.value = response.data;
} catch (err) { console.error('Positions fetch failed'); }
}
async function fetchResults() {
try {
const response = await voteService.getResults();
results.value = response.data;
} catch (err) { console.error('Results fetch failed'); }
}
async function addPosition() {
loading.value = true;
try {
await positionService.create(newPosition);
newPosition.title = ''; newPosition.display_order = 0;
await fetchPositions();
} catch (err) { alert('Failed to add position'); }
finally { loading.value = false; }
}
function handleImageUpload(e) { newCandidate.image = e.target.files[0]; }
async function addCandidate() {
loading.value = true;
const formData = new FormData();
formData.append('name', newCandidate.name);
formData.append('position_id', newCandidate.position_id);
formData.append('image', newCandidate.image);
try {
await candidateService.create(formData);
alert('Candidate Added!');
newCandidate.name = ''; newCandidate.position_id = ''; newCandidate.image = null;
fetchResults();
} catch (err) { alert('Save failed'); }
finally { loading.value = false; }
}
async function deleteCandidate(id) {
if (!confirm('Are you sure you want to delete this candidate? This will also remove all votes they received.')) return;
try {
await candidateService.delete(id);
await fetchResults();
} catch (err) {
alert(err.response?.data?.error || 'Failed to delete candidate');
}
}
async function handleReset() {
if (!confirm('This is the FINAL check. Continue?')) return;
try {
await voteService.resetVotes(resetConfirm);
alert('System Reset Successfully.');
closeResetModal();
fetchResults();
} catch (err) { alert('Reset Failed'); }
}
function closeResetModal() {
showResetModal.value = false;
resetConfirm.adminPassword = ''; resetConfirm.resetPassword = ''; resetConfirm.sentence = '';
}
</script>
<style scoped>
.admin-page {
min-height: 100vh;
background-color: var(--bg-color);
padding: 2rem;
}
.admin-container {
max-width: 1200px;
margin: 0 auto;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.admin-header h1 {
font-size: 2rem;
color: var(--accent-color);
}
.dashboard-layout {
display: grid;
grid-template-columns: 400px 1fr;
gap: 2rem;
}
.card {
background: var(--card-bg);
padding: 2rem;
border-radius: 12px;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
}
h3 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
color: var(--text-primary);
}
.form-group {
margin-bottom: 1.25rem;
}
label {
display: block;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
input, .admin-select {
width: 100%;
background: #252932;
border: 1px solid var(--border-color);
padding: 0.8rem;
border-radius: 6px;
color: var(--text-primary);
box-sizing: border-box;
}
.admin-select {
cursor: pointer;
}
.positions-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.pos-tag {
background: #252932;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.8rem;
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.hidden-input {
display: none;
}
.file-label {
display: block;
background: #252932;
border: 1px dashed var(--border-color);
padding: 1rem;
text-align: center;
border-radius: 6px;
cursor: pointer;
color: var(--text-secondary);
}
.file-label:hover {
border-color: var(--accent-color);
}
.primary-btn {
width: 100%;
background: var(--accent-color);
color: #fff;
padding: 1rem;
border: none;
border-radius: 6px;
font-weight: 700;
cursor: pointer;
}
.secondary-btn {
background: #252932;
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 0.6rem 1.2rem;
border-radius: 6px;
cursor: pointer;
}
.danger-card {
border-color: rgba(231, 76, 60, 0.3);
}
.danger-card h3 {
color: var(--danger-color);
}
.danger-btn {
background: var(--danger-color);
color: #fff;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 6px;
font-weight: 700;
cursor: pointer;
}
/* Results */
.position-group {
margin-bottom: 3rem;
}
.group-title {
color: var(--accent-color);
font-size: 1.1rem;
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.result-row {
margin-bottom: 1.5rem;
}
.row-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
align-items: center;
}
.cand-details-admin {
display: flex;
align-items: center;
gap: 0.8rem;
}
.delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
padding: 0;
opacity: 0.5;
transition: opacity 0.2s;
}
.delete-btn:hover {
opacity: 1;
}
.cand-name {
font-weight: 700;
color: var(--text-primary);
}
.vote-count {
font-weight: 800;
color: var(--accent-color);
}
.progress-track {
height: 10px;
background: #252932;
border-radius: 5px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent-color);
border-radius: 5px;
transition: width 1s ease-out;
}
.hint {
font-size: 0.8rem;
color: var(--danger-color);
margin-top: 0.5rem;
}
/* Modal */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: var(--card-bg);
padding: 3rem;
border-radius: 20px;
border: 1px solid var(--border-color);
max-width: 500px;
width: 90%;
text-align: center;
}
.warning-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.confirm-inputs {
margin: 2rem 0;
text-align: left;
}
.final-reset-btn {
width: 100%;
background: var(--danger-color);
color: #fff;
padding: 1rem;
border: none;
border-radius: 6px;
font-weight: 800;
margin-bottom: 1rem;
cursor: pointer;
}
.final-reset-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cancel-btn {
background: transparent;
color: var(--text-secondary);
border: none;
cursor: pointer;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>