{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.polls.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ t('ui.admin.polls.heading', {}, locale, ['common']) }}</h1>
<button type="button" class="btn btn-primary" onclick="showCreatePollModal()">
<i class="fas fa-plus me-2"></i>{{ t('ui.admin.polls.add_poll', {}, locale, ['common']) }}
</button>
</div>
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="pollsTable">
<thead class="table-dark">
<tr>
<th>{{ t('ui.common.id', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.question', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.status', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.votes', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.created_by', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.created', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.polls.actions', {}, locale, ['common']) }}</th>
</tr>
</thead>
<tbody id="pollsTableBody">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="pollModal" tabindex="-1" aria-labelledby="pollModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pollModalLabel">{{ t('ui.admin.polls.add_poll', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, locale, ['common']) }}"></button>
</div>
<div class="modal-body">
<form id="pollForm">
<input type="hidden" id="pollId" name="pollId">
<div class="mb-3" id="createdByContainer" style="display: none;">
<label class="form-label">{{ t('ui.admin.polls.created_by', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="pollCreatedBy" readonly>
</div>
<div class="mb-3">
<label for="pollQuestion" class="form-label">{{ t('ui.admin.polls.question', {}, locale, ['common']) }} <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="pollQuestion" name="question" required>
</div>
<div class="mb-3">
<label class="form-label">{{ t('ui.admin.polls.options', {}, locale, ['common']) }} <span class="text-danger">*</span></label>
<div id="pollOptions">
<!-- Option inputs -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addPollOption()">
<i class="fas fa-plus me-1"></i>{{ t('ui.admin.polls.add_option', {}, locale, ['common']) }}
</button>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pollActive" name="is_active" checked>
<label class="form-check-label" for="pollActive">
{{ t('ui.admin.polls.active_poll', {}, locale, ['common']) }}
</label>
</div>
<div class="form-text text-muted mt-2">
{{ t('ui.admin.polls.editing_options_resets_votes', {}, locale, ['common']) }}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-primary" onclick="savePoll()">{{ t('ui.admin.polls.save_poll', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
</div>
<script>
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
loadI18nNamespaces(['common', 'errors']).then(function() {
loadPolls();
});
});
function loadPolls() {
fetch('/admin/api/polls')
.then(response => response.json())
.then(data => {
displayPolls(data.polls || []);
})
.catch(error => {
console.error('Error loading polls:', error);
showAlert(uiT('ui.admin.polls.load_failed', 'Error loading polls'), 'danger');
});
}
function displayPolls(polls) {
const tbody = document.getElementById('pollsTableBody');
tbody.innerHTML = '';
polls.forEach(poll => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${poll.id}</td>
<td>${escapeHtml(poll.question)}</td>
<td>
<span class="badge bg-${poll.is_active ? 'success' : 'secondary'}">
${poll.is_active ? uiT('ui.admin.polls.active', 'Active') : uiT('ui.admin.polls.inactive', 'Inactive')}
</span>
</td>
<td>${poll.vote_count || 0}</td>
<td>${escapeHtml(poll.created_by_username || uiT('ui.common.unknown', 'Unknown'))}</td>
<td>${new Date(poll.created_at).toLocaleDateString()}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="editPoll(${poll.id})" title="${uiT('ui.common.edit', 'Edit')}">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deletePoll(${poll.id})" title="${uiT('ui.common.delete', 'Delete')}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
}
function showCreatePollModal() {
document.getElementById('pollModalLabel').textContent = uiT('ui.admin.polls.add_poll', 'Add Poll');
document.getElementById('pollForm').reset();
document.getElementById('pollId').value = '';
document.getElementById('pollActive').checked = true;
document.getElementById('createdByContainer').style.display = 'none';
resetPollOptions(['', '']);
new bootstrap.Modal(document.getElementById('pollModal')).show();
}
function editPoll(pollId) {
fetch('/admin/api/polls')
.then(response => response.json())
.then(data => {
const poll = (data.polls || []).find(item => item.id === pollId);
if (!poll) {
showAlert(uiT('ui.admin.polls.not_found', 'Poll not found'), 'danger');
return;
}
document.getElementById('pollModalLabel').textContent = uiT('ui.admin.polls.edit_poll', 'Edit Poll');
document.getElementById('pollId').value = poll.id;
document.getElementById('pollQuestion').value = poll.question;
document.getElementById('pollActive').checked = !!poll.is_active;
if (poll.created_by_username) {
document.getElementById('pollCreatedBy').value = poll.created_by_username;
document.getElementById('createdByContainer').style.display = 'block';
} else {
document.getElementById('createdByContainer').style.display = 'none';
}
const options = (poll.options || []).map(opt => opt.option_text);
resetPollOptions(options.length ? options : ['', '']);
new bootstrap.Modal(document.getElementById('pollModal')).show();
})
.catch(error => {
console.error('Error loading poll:', error);
showAlert(uiT('ui.admin.polls.load_single_failed', 'Error loading poll'), 'danger');
});
}
function resetPollOptions(options) {
const optionsContainer = document.getElementById('pollOptions');
optionsContainer.innerHTML = '';
options.forEach(optionText => addPollOption(optionText));
}
function addPollOption(value = '') {
const optionsContainer = document.getElementById('pollOptions');
const optionRow = document.createElement('div');
optionRow.className = 'input-group mb-2';
optionRow.innerHTML = `
<input type="text" class="form-control poll-option" value="${escapeHtml(value)}" placeholder="${escapeHtml(uiT('ui.admin.polls.option_text_placeholder', 'Option text'))}">
<button class="btn btn-outline-danger" type="button" onclick="removePollOption(this)">
<i class="fas fa-times"></i>
</button>
`;
optionsContainer.appendChild(optionRow);
}
function removePollOption(button) {
const optionsContainer = document.getElementById('pollOptions');
if (optionsContainer.children.length <= 2) {
alert(uiT('ui.admin.polls.min_options_required', 'At least two options are required.'));
return;
}
button.closest('.input-group').remove();
}
function savePoll() {
const pollId = document.getElementById('pollId').value;
const question = document.getElementById('pollQuestion').value.trim();
const isActive = document.getElementById('pollActive').checked;
const options = Array.from(document.querySelectorAll('.poll-option'))
.map(input => input.value.trim())
.filter(text => text !== '');
if (!question) {
alert(uiT('ui.admin.polls.question_required', 'Question is required.'));
return;
}
if (options.length < 2) {
alert(uiT('ui.admin.polls.provide_min_options', 'Please provide at least two options.'));
return;
}
const payload = { question, options, is_active: isActive };
const url = pollId ? `/admin/api/polls/${pollId}` : '/admin/api/polls';
const method = pollId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.success || data.id) {
const successMessage = window.getApiMessage
? window.getApiMessage(
data,
pollId
? uiT('ui.admin.polls.updated_success', 'Poll updated successfully')
: uiT('ui.admin.polls.created_success', 'Poll created successfully')
)
: (pollId
? uiT('ui.admin.polls.updated_success', 'Poll updated successfully')
: uiT('ui.admin.polls.created_success', 'Poll created successfully'));
showAlert(successMessage, 'success');
bootstrap.Modal.getInstance(document.getElementById('pollModal')).hide();
loadPolls();
} else {
showAlert(apiError(data, uiT('ui.admin.polls.save_failed', 'Error saving poll')), 'danger');
}
})
.catch(error => {
console.error('Error saving poll:', error);
showAlert(uiT('ui.admin.polls.save_failed', 'Error saving poll'), 'danger');
});
}
function deletePoll(pollId) {
if (!confirm(uiT('ui.admin.polls.delete_confirm', 'Delete this poll? This cannot be undone.'))) {
return;
}
fetch(`/admin/api/polls/${pollId}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.success) {
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.polls.deleted_success', 'Poll deleted successfully'))
: uiT('ui.admin.polls.deleted_success', 'Poll deleted successfully');
showAlert(successMessage, 'success');
loadPolls();
} else {
showAlert(apiError(data, uiT('ui.admin.polls.delete_failed', 'Error deleting poll')), 'danger');
}
})
.catch(error => {
console.error('Error deleting poll:', error);
showAlert(uiT('ui.admin.polls.delete_failed', 'Error deleting poll'), 'danger');
});
}
function escapeHtml(text) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container-fluid');
container.insertBefore(alertDiv, container.firstChild);
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script>
{% endblock %}
|