diff --git a/resources/views/certificate-backend/index.blade.php b/resources/views/certificate-backend/index.blade.php
index 7764f9839..25e68c11a 100644
--- a/resources/views/certificate-backend/index.blade.php
+++ b/resources/views/certificate-backend/index.blade.php
@@ -42,6 +42,8 @@
{{-- Visible error (in case alerts are blocked) --}}
+ {{-- Success message (e.g. "Generation started" + queue reminder) --}}
+
{{-- Stats --}}
@@ -125,7 +127,8 @@
(function() {
const editionSelect = document.getElementById('edition-select');
const typeSlug = '{{ $typeSlug }}';
- const basePath = '{{ url("/admin/certificate-backend") }}'.replace(/\/$/, '');
+ // Relative path so POST is same-origin (session + CSRF work reliably)
+ const basePath = '/admin/certificate-backend';
let currentPage = 1;
let searchQuery = '';
let statusInterval = null;
@@ -137,10 +140,21 @@ function showError(msg) {
el.classList.toggle('hidden', !msg);
}
+ function showSuccess(msg) {
+ const el = document.getElementById('last-success');
+ if (!el) return;
+ el.textContent = msg || '';
+ el.classList.toggle('hidden', !msg);
+ }
+
function clearError() {
showError('');
}
+ function clearSuccess() {
+ showSuccess('');
+ }
+
function apiUrl(path, params = {}) {
const segment = path.replace(/^\//, '');
const u = new URL(segment ? basePath + '/' + segment : basePath, window.location.origin);
@@ -178,13 +192,17 @@ function fetchJson(url, options = {}) {
}
function postJson(url, body = {}) {
+ const token = csrf();
+ if (!token) {
+ return Promise.reject(new Error('CSRF token missing. Refresh the page and try again.'));
+ }
return fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
- 'X-CSRF-TOKEN': csrf(),
+ 'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(body)
@@ -227,6 +245,18 @@ function updateProgress(data) {
}
}
+ /** Start polling status every 2s for a while after starting an action (so progress bars appear once the worker picks up the job). */
+ function startStatusPollingForAWhile() {
+ if (statusInterval) return;
+ statusInterval = setInterval(loadStatus, 2000);
+ setTimeout(function() {
+ if (statusInterval) {
+ clearInterval(statusInterval);
+ statusInterval = null;
+ }
+ }, 120000);
+ }
+
function loadStatus() {
fetchJson(apiUrl('/status')).then(data => {
document.getElementById('stat-total').textContent = data.total ?? '–';
@@ -295,38 +325,95 @@ function pagination(current, last, total) {
document.getElementById('btn-generate').addEventListener('click', function(e) {
e.preventDefault();
const btn = this;
+ const origText = btn.textContent;
clearError();
+ clearSuccess();
btn.disabled = true;
+ btn.textContent = 'Starting…';
postJson(apiUrl('/generate/start')).then(r => {
- showError(r.ok ? '' : (r.message || 'Error'));
- if (r.ok) loadStatus();
- }).catch(function(err) { showError(err.message || 'Request failed.'); }).finally(function() { btn.disabled = false; });
+ if (r.ok) {
+ showError('');
+ showSuccess('Generation started. Progress will update above. If the numbers don\'t change within a minute, run the queue worker on the server: php artisan queue:work');
+ loadStatus();
+ startStatusPollingForAWhile();
+ } else {
+ showError(r.message || 'Error');
+ showSuccess('');
+ }
+ }).catch(function(err) {
+ showError(err.message || 'Request failed.');
+ showSuccess('');
+ }).finally(function() {
+ btn.disabled = false;
+ btn.textContent = origText;
+ });
});
document.getElementById('btn-cancel').addEventListener('click', function(e) {
e.preventDefault();
clearError();
- postJson(apiUrl('/generate/cancel')).then(r => { showError(r.ok ? '' : r.message); loadStatus(); }).catch(function(err) { showError(err.message || 'Request failed.'); });
+ clearSuccess();
+ postJson(apiUrl('/generate/cancel')).then(r => {
+ showError(r.ok ? '' : r.message);
+ showSuccess(r.ok ? 'Cancellation requested.' : '');
+ loadStatus();
+ }).catch(function(err) { showError(err.message || 'Request failed.'); });
});
document.getElementById('btn-send').addEventListener('click', function(e) {
e.preventDefault();
+ const btn = this;
+ const origText = btn.textContent;
clearError();
+ clearSuccess();
+ btn.disabled = true;
+ btn.textContent = 'Starting…';
postJson(apiUrl('/send/start')).then(r => {
- showError(r.ok ? '' : (r.message || 'Error'));
- if (r.ok) loadStatus();
- loadList(currentPage);
- }).catch(function(err) { showError(err.message || 'Request failed.'); });
+ if (r.ok) {
+ showError('');
+ showSuccess('Sending started. Progress will update above. If the numbers don\'t change, run the queue worker: php artisan queue:work');
+ loadStatus();
+ loadList(currentPage);
+ startStatusPollingForAWhile();
+ } else {
+ showError(r.message || 'Error');
+ showSuccess('');
+ }
+ }).catch(function(err) {
+ showError(err.message || 'Request failed.');
+ showSuccess('');
+ }).finally(function() {
+ btn.disabled = false;
+ btn.textContent = origText;
+ });
});
document.getElementById('btn-resend-all-failed').addEventListener('click', function(e) {
e.preventDefault();
+ const btn = this;
+ const origText = btn.textContent;
clearError();
+ clearSuccess();
+ btn.disabled = true;
+ btn.textContent = 'Starting…';
postJson(apiUrl('/resend-all-failed')).then(r => {
- showError(r.ok ? '' : (r.message || 'Error'));
- if (r.ok) loadStatus();
- loadList(currentPage);
- }).catch(function(err) { showError(err.message || 'Request failed.'); });
+ if (r.ok) {
+ showError('');
+ showSuccess('Resend started. Progress will update above. Run the queue worker if needed: php artisan queue:work');
+ loadStatus();
+ loadList(currentPage);
+ startStatusPollingForAWhile();
+ } else {
+ showError(r.message || 'Error');
+ showSuccess('');
+ }
+ }).catch(function(err) {
+ showError(err.message || 'Request failed.');
+ showSuccess('');
+ }).finally(function() {
+ btn.disabled = false;
+ btn.textContent = origText;
+ });
});
document.getElementById('btn-manual-generate').addEventListener('click', function(e) {
@@ -373,7 +460,7 @@ function pagination(current, last, total) {
const id = regenerateBtn.dataset.id;
regenerateBtn.disabled = true;
clearError();
- const url = '{{ url("/admin/certificate-backend/regenerate") }}'.replace(/\/$/, '') + '/' + id;
+ const url = basePath + '/regenerate/' + id + '?edition=' + encodeURIComponent(editionSelect.value) + '&type=' + encodeURIComponent(typeSlug);
postJson(url, {}).then(r => {
showError(r.ok ? '' : r.message);
if (r.ok) { loadStatus(); loadList(currentPage); }
@@ -384,7 +471,7 @@ function pagination(current, last, total) {
const id = resendBtn.dataset.id;
resendBtn.disabled = true;
clearError();
- const resendUrl = '{{ url("/admin/certificate-backend/resend") }}'.replace(/\/$/, '') + '/' + id;
+ const resendUrl = basePath + '/resend/' + id + '?edition=' + encodeURIComponent(editionSelect.value) + '&type=' + encodeURIComponent(typeSlug);
postJson(resendUrl, {}).then(r => {
showError(r.ok ? '' : r.message);
if (r.ok) { loadStatus(); loadList(currentPage); }