This commit implements a comprehensive status color system across the dashboard and validation interfaces, ensuring visual consistency between table rows and filter buttons. Color System Changes: - Updated statusRowBg mapping in script_requests.php with custom hex colors: * Pend: white (#ffffff) with black text * PartColl: pink (#ff99aa) with black text * Coll: red (#d63031) with white text * PartRecv: light blue (#a0c0d9) with black text * Recv: blue (#0984e3) with white text * Inc: yellow (#ffff00) with black text * Fin: green (#008000) with white text - Added custom CSS button classes in layout.php matching row background colors - Applied color backgrounds to table rows (Order through Tests columns) - Removed hardcoded text-white classes, now using dynamic text colors from mapping UI/UX Improvements: - Table rows now have consistent color-coded backgrounds based on request status - Filter button badges match their corresponding row background colors - Yellow status uses black text for better readability - Swapped Coll (yellow) and Inc (orange) colors as requested Validation Dialog Enhancement: - Updated dialog_val.php iframe to use dynamic URL generation - Removed preview type selection (ID, EN, PDF options) - uses default only - Added getPreviewUrl() method in script_validation.php - Now uses same URL pattern as preview dialog: http://glenlis/spooler_db/main_dev.php?acc={accessnumber} Documentation Updates: - Added Serena MCP tool usage guidelines to AGENTS.md - Renamed CHECKLIST.md to TODO.md - Removed CLAUDE.md Technical Details: - Color mappings now include both background and text color classes - Implemented using Tailwind arbitrary values for precise hex color matching - Status buttons use btn-status-{status} and badge-status-{status} classes - All 7 columns from Order through Tests have status-colored backgrounds
218 lines
5.7 KiB
PHP
218 lines
5.7 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,
|
|
|
|
sort(col) {
|
|
if (this.sortCol === col) {
|
|
this.sortAsc = !this.sortAsc;
|
|
} else {
|
|
this.sortCol = col;
|
|
this.sortAsc = true;
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.currentPage < this.unvalidatedTotalPages) this.currentPage++;
|
|
},
|
|
|
|
prevPage() {
|
|
if (this.currentPage > 1) this.currentPage--;
|
|
},
|
|
|
|
get unvalidatedTotalPages() {
|
|
return Math.ceil(this.unvalidatedFiltered.length / this.pageSize) || 1;
|
|
},
|
|
|
|
get unvalidatedSorted() {
|
|
return 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;
|
|
});
|
|
},
|
|
|
|
get unvalidatedPaginated() {
|
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
const end = start + this.pageSize;
|
|
return this.unvalidatedSorted.slice(start, end);
|
|
},
|
|
|
|
get unvalidatedFiltered() {
|
|
if (!this.filterTable) return this.unvalidatedList;
|
|
const searchTerm = this.filterTable.toLowerCase();
|
|
return this.unvalidatedList.filter(item =>
|
|
Object.values(item).some(value =>
|
|
String(value).toLowerCase().includes(searchTerm)
|
|
)
|
|
);
|
|
},
|
|
|
|
get unvalidatedCount() {
|
|
return this.unvalidatedList.length;
|
|
},
|
|
|
|
init() {
|
|
this.today = new Date().toISOString().slice(0, 10);
|
|
|
|
this.filter.date1 = this.today;
|
|
this.filter.date2 = this.today;
|
|
|
|
this.$watch('filterTable', () => {
|
|
this.currentPage = 1;
|
|
});
|
|
|
|
// Auto-fetch on page load
|
|
this.fetchUnvalidated();
|
|
|
|
// Keyboard shortcuts for dialog
|
|
document.addEventListener('keydown', (e) => {
|
|
if (this.isDialogValOpen) {
|
|
// N key - skip to next
|
|
if (e.key === 'n' || e.key === 'N') {
|
|
if (!e.target.closest('input, textarea, button')) {
|
|
e.preventDefault();
|
|
this.skipToNext();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
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 ?? [];
|
|
}).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,
|
|
toast: { show: false, message: '', type: 'success' },
|
|
|
|
showToast(message, type = 'success') {
|
|
this.toast = { show: true, message, type };
|
|
setTimeout(() => {
|
|
this.toast.show = false;
|
|
}, 2000);
|
|
},
|
|
|
|
openValDialogByIndex(index) {
|
|
const filtered = this.unvalidatedFiltered;
|
|
if (index < 0 || index >= filtered.length) {
|
|
this.showToast('No more requests', 'error');
|
|
this.closeValDialog();
|
|
return;
|
|
}
|
|
const item = filtered[index];
|
|
this.currentIndex = index;
|
|
this.valAccessnumber = item.SP_ACCESSNUMBER;
|
|
this.valItem = item;
|
|
this.isDialogValOpen = true;
|
|
// Focus validate button after dialog renders - use setTimeout for reliability
|
|
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;
|
|
},
|
|
|
|
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 (!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 => {
|
|
// Show toast
|
|
this.showToast(`Validated: ${accessnumber}`);
|
|
|
|
// Remove validated item from local list
|
|
this.unvalidatedList = this.unvalidatedList.filter(
|
|
item => item.SP_ACCESSNUMBER !== accessnumber
|
|
);
|
|
|
|
// Auto-advance to next request
|
|
const filteredLength = this.unvalidatedFiltered.length;
|
|
if (filteredLength > 0) {
|
|
const nextIndex = Math.min(this.currentIndex, filteredLength - 1);
|
|
this.closeValDialog();
|
|
// Use setTimeout for reliable focus after dialog re-renders
|
|
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() {
|
|
let base = 'http://glenlis/spooler_db/main_dev.php';
|
|
return `${base}?acc=${this.valAccessnumber}`;
|
|
},
|
|
}));
|
|
});
|