Use the local accessnumber when queuing PDFs after val2, since the validation response does not include accessnumber. Add success and error toast feedback for the PDF generation request.
301 lines
8.5 KiB
PHP
301 lines
8.5 KiB
PHP
document.addEventListener('alpine:init', () => {
|
|
Alpine.data("validatePage", () => ({
|
|
// validate page specific
|
|
unvalidatedList: [],
|
|
isLoading: false,
|
|
isValidating: false,
|
|
|
|
// Date filter - missing in original code!
|
|
filter: { date1: "", date2: "" },
|
|
filterTable: "",
|
|
|
|
// Sorting & Pagination (shared with dashboard)
|
|
sortCol: 'REQDATE',
|
|
sortAsc: false,
|
|
currentPage: 1,
|
|
pageSize: 30,
|
|
|
|
// Cached computed properties to prevent memory leak
|
|
unvalidatedFiltered: [],
|
|
unvalidatedSorted: [],
|
|
unvalidatedPaginated: [],
|
|
unvalidatedTotalPages: 1,
|
|
unvalidatedCount: 0,
|
|
|
|
sort(col) {
|
|
if (this.sortCol === col) {
|
|
this.sortAsc = !this.sortAsc;
|
|
} else {
|
|
this.sortCol = col;
|
|
this.sortAsc = true;
|
|
}
|
|
this.computeUnvalidatedSorted();
|
|
this.computeUnvalidatedPaginated();
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.currentPage < this.unvalidatedTotalPages) {
|
|
this.currentPage++;
|
|
this.computeUnvalidatedPaginated();
|
|
}
|
|
},
|
|
|
|
prevPage() {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
this.computeUnvalidatedPaginated();
|
|
}
|
|
},
|
|
|
|
setFilterTable(value) {
|
|
this.filterTable = value;
|
|
this.currentPage = 1;
|
|
this.computeUnvalidatedFiltered();
|
|
this.computeUnvalidatedSorted();
|
|
this.computeUnvalidatedTotalPages();
|
|
this.computeUnvalidatedPaginated();
|
|
},
|
|
|
|
// Compute methods - called only when dependencies change
|
|
computeUnvalidatedFiltered() {
|
|
if (!this.filterTable) {
|
|
this.unvalidatedFiltered = this.unvalidatedList;
|
|
} else {
|
|
const searchTerm = this.filterTable.toLowerCase();
|
|
this.unvalidatedFiltered = this.unvalidatedList.filter(item =>
|
|
Object.values(item).some(value =>
|
|
String(value).toLowerCase().includes(searchTerm)
|
|
)
|
|
);
|
|
}
|
|
},
|
|
|
|
computeUnvalidatedSorted() {
|
|
this.unvalidatedSorted = this.unvalidatedFiltered.slice().sort((a, b) => {
|
|
let modifier = this.sortAsc ? 1 : -1;
|
|
if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier;
|
|
if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier;
|
|
return 0;
|
|
});
|
|
},
|
|
|
|
computeUnvalidatedPaginated() {
|
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
const end = start + this.pageSize;
|
|
this.unvalidatedPaginated = this.unvalidatedSorted.slice(start, end);
|
|
},
|
|
|
|
computeUnvalidatedTotalPages() {
|
|
this.unvalidatedTotalPages = Math.ceil(this.unvalidatedFiltered.length / this.pageSize) || 1;
|
|
},
|
|
|
|
computeUnvalidatedCount() {
|
|
this.unvalidatedCount = this.unvalidatedList.length;
|
|
},
|
|
|
|
init() {
|
|
this.today = new Date().toISOString().slice(0, 10);
|
|
|
|
this.filter.date1 = this.today;
|
|
this.filter.date2 = this.today;
|
|
|
|
// Initial load only - no auto-refresh
|
|
this.fetchUnvalidated();
|
|
|
|
// Keyboard shortcuts - added/removed when dialog opens/closes
|
|
this._keyboardHandler = (e) => {
|
|
if (this.isDialogValOpen) {
|
|
if (e.key === 'n' || e.key === 'N') {
|
|
if (!e.target.closest('input, textarea, button')) {
|
|
e.preventDefault();
|
|
this.skipToNext();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener('keydown', this._keyboardHandler);
|
|
},
|
|
|
|
fetchUnvalidated() {
|
|
this.isLoading = true;
|
|
this.unvalidatedList = [];
|
|
let param = new URLSearchParams(this.filter).toString();
|
|
fetch(`${BASEURL}/api/validate/unvalidated?${param}`, {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}).then(res => res.json()).then(data => {
|
|
this.unvalidatedList = data.data ?? [];
|
|
// Compute derived data after list is loaded
|
|
this.computeUnvalidatedFiltered();
|
|
this.computeUnvalidatedCount();
|
|
this.computeUnvalidatedSorted();
|
|
this.computeUnvalidatedTotalPages();
|
|
this.computeUnvalidatedPaginated();
|
|
}).finally(() => {
|
|
this.isLoading = false;
|
|
});
|
|
},
|
|
|
|
resetUnvalidated() {
|
|
this.filter.date1 = this.today;
|
|
this.filter.date2 = this.today;
|
|
this.fetchUnvalidated();
|
|
},
|
|
|
|
/*
|
|
* validate dialog methods
|
|
*/
|
|
valAccessnumber: null,
|
|
valItem: null,
|
|
currentIndex: 0,
|
|
isDialogValOpen: false,
|
|
isIframeLoaded: false,
|
|
validationDelayRemaining: 0,
|
|
validationTimer: null,
|
|
toast: { show: false, message: '', type: 'success' },
|
|
|
|
showToast(message, type = 'success') {
|
|
this.toast = { show: true, message, type };
|
|
setTimeout(() => {
|
|
this.toast.show = false;
|
|
}, 2000);
|
|
},
|
|
|
|
onIframeLoad() {
|
|
this.isIframeLoaded = true;
|
|
this.validationDelayRemaining = 1;
|
|
this.validationTimer = setInterval(() => {
|
|
this.validationDelayRemaining--;
|
|
if (this.validationDelayRemaining <= 0) {
|
|
clearInterval(this.validationTimer);
|
|
this.validationTimer = null;
|
|
}
|
|
}, 1000);
|
|
},
|
|
|
|
openValDialogByIndex(index) {
|
|
const filtered = this.unvalidatedFiltered;
|
|
if (index < 0 || index >= filtered.length) {
|
|
this.showToast('No more requests', 'error');
|
|
this.closeValDialog();
|
|
return;
|
|
}
|
|
// Reset iframe loading state
|
|
this.isIframeLoaded = false;
|
|
this.validationDelayRemaining = 0;
|
|
if (this.validationTimer) {
|
|
clearInterval(this.validationTimer);
|
|
this.validationTimer = null;
|
|
}
|
|
const item = filtered[index];
|
|
this.currentIndex = index;
|
|
this.valAccessnumber = item.SP_ACCESSNUMBER;
|
|
this.valItem = item;
|
|
this.isDialogValOpen = true;
|
|
// Re-add keyboard handler when dialog opens
|
|
if (this._keyboardHandler) {
|
|
document.addEventListener('keydown', this._keyboardHandler);
|
|
}
|
|
setTimeout(() => {
|
|
const btn = document.getElementById('validate-btn');
|
|
if (btn) btn.focus();
|
|
}, 50);
|
|
},
|
|
|
|
openValDialog(accessnumber) {
|
|
// Find index by accessnumber
|
|
const filtered = this.unvalidatedFiltered;
|
|
const index = filtered.findIndex(item => item.SP_ACCESSNUMBER === accessnumber);
|
|
if (index !== -1) {
|
|
this.openValDialogByIndex(index);
|
|
} else {
|
|
this.openValDialogByIndex(0);
|
|
}
|
|
},
|
|
|
|
closeValDialog() {
|
|
this.isDialogValOpen = false;
|
|
this.valAccessnumber = null;
|
|
this.valItem = null;
|
|
if (this.validationTimer) {
|
|
clearInterval(this.validationTimer);
|
|
this.validationTimer = null;
|
|
}
|
|
this.validationDelayRemaining = 0;
|
|
this.isIframeLoaded = false;
|
|
// Remove keyboard handler when dialog closes
|
|
if (this._keyboardHandler) {
|
|
document.removeEventListener('keydown', this._keyboardHandler);
|
|
this._keyboardHandler = null;
|
|
}
|
|
},
|
|
|
|
skipToNext() {
|
|
const nextIndex = (this.currentIndex + 1) % this.unvalidatedFiltered.length;
|
|
this.closeValDialog();
|
|
// Use setTimeout for reliable focus after dialog re-renders
|
|
setTimeout(() => this.openValDialogByIndex(nextIndex), 50);
|
|
},
|
|
|
|
validate(accessnumber, userid) {
|
|
if (!this.isIframeLoaded || this.validationDelayRemaining > 0) {
|
|
this.showToast('Please wait for the report to load', 'error');
|
|
return;
|
|
}
|
|
if (!confirm(`Validate request ${accessnumber}?`)) return;
|
|
this.isValidating = true;
|
|
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userid: `${userid}` })
|
|
}).then(response => response.json()).then(data => {
|
|
console.log(data);
|
|
if (data.val === 2) {
|
|
this.showToast(`Validated (val2): ${accessnumber} - PDF queued`);
|
|
// Trigger PDF auto-generation after val2
|
|
fetch(`${BASEURL}/report/${accessnumber}/pdf`).then(res => res.json()).then(pdfData => {
|
|
if (pdfData.success) {
|
|
console.log('PDF generation queued:', pdfData.jobId);
|
|
this.showToast(`${pdfData.lang} PDF queued for generation`, 'success');
|
|
} else {
|
|
console.error('PDF generation failed:', pdfData.error);
|
|
this.showToast('PDF generation failed', 'error');
|
|
}
|
|
}).catch(err => {
|
|
console.error('PDF generation request failed:', err);
|
|
this.showToast('PDF generation failed', 'error');
|
|
});
|
|
} else {
|
|
this.showToast(`Validated: ${accessnumber}`);
|
|
}
|
|
|
|
this.unvalidatedList = this.unvalidatedList.filter(
|
|
item => item.SP_ACCESSNUMBER !== accessnumber
|
|
);
|
|
|
|
const filteredLength = this.unvalidatedFiltered.length;
|
|
if (filteredLength > 0) {
|
|
const nextIndex = Math.min(this.currentIndex, filteredLength - 1);
|
|
this.closeValDialog();
|
|
setTimeout(() => this.openValDialogByIndex(nextIndex), 50);
|
|
} else {
|
|
this.closeValDialog();
|
|
this.showToast('All requests validated!');
|
|
}
|
|
|
|
if (data.message && data.message.includes('already validate')) {
|
|
alert(data.message);
|
|
}
|
|
}).catch(() => {
|
|
this.showToast('Validation failed', 'error');
|
|
}).finally(() => {
|
|
this.isValidating = false;
|
|
});
|
|
},
|
|
|
|
getPreviewUrl() {
|
|
return `<?=base_url()?>report/${this.valAccessnumber}`;
|
|
},
|
|
}));
|
|
});
|