633 lines
21 KiB
JavaScript
633 lines
21 KiB
JavaScript
const POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
|
const API_BASE = '/api/reports';
|
|
const EMPLOYEES_API = '/api/employees';
|
|
|
|
// Initialize PocketBase client
|
|
const pb = new PocketBase(POCKETBASE_URL);
|
|
|
|
// Load auth from localStorage/cookies
|
|
try {
|
|
pb.authStore.loadFromCookie(document.cookie);
|
|
} catch (e) {
|
|
const storedToken = localStorage.getItem('pb_auth_token');
|
|
const storedModel = localStorage.getItem('pb_auth_model');
|
|
if (storedToken && storedModel) {
|
|
try {
|
|
pb.authStore.save(storedToken, JSON.parse(storedModel));
|
|
} catch (e2) {
|
|
console.error('Error loading auth:', e2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get auth token for API requests
|
|
function getAuthToken() {
|
|
// Try to get from PocketBase auth store first
|
|
if (pb.authStore.token) {
|
|
return pb.authStore.token;
|
|
}
|
|
|
|
// Fallback to localStorage
|
|
const storedToken = localStorage.getItem('pb_auth_token');
|
|
if (storedToken) {
|
|
// Also update the auth store with the token
|
|
const storedModel = localStorage.getItem('pb_auth_model');
|
|
if (storedModel) {
|
|
try {
|
|
pb.authStore.save(storedToken, JSON.parse(storedModel));
|
|
} catch (e) {
|
|
console.error('Error loading auth from localStorage:', e);
|
|
}
|
|
}
|
|
return storedToken;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
// Load employees for dropdown
|
|
let employeesList = [];
|
|
|
|
// Rich text editor instance
|
|
let reportEditor = null;
|
|
|
|
async function loadEmployees() {
|
|
try {
|
|
const token = getAuthToken();
|
|
if (!token) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`${EMPLOYEES_API}?page=1&perPage=1000`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load employees');
|
|
}
|
|
|
|
const data = await response.json();
|
|
employeesList = data.items || [];
|
|
populateEmployeeDropdown();
|
|
} catch (error) {
|
|
console.error('Error loading employees:', error);
|
|
}
|
|
}
|
|
|
|
function populateEmployeeDropdown() {
|
|
const employeeSelect = document.getElementById('Employee');
|
|
if (!employeeSelect) return;
|
|
|
|
// Clear existing options except the first one
|
|
while (employeeSelect.options.length > 1) {
|
|
employeeSelect.remove(1);
|
|
}
|
|
|
|
employeesList.forEach(emp => {
|
|
const option = document.createElement('option');
|
|
option.value = emp.id;
|
|
const name = `${emp.First_Name || ''} ${emp.Last_Name || ''}`.trim() || emp.id;
|
|
option.textContent = `${name} (${emp.id})`;
|
|
employeeSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Show message
|
|
function showMessage(message, type = 'success') {
|
|
const container = document.getElementById('messageContainer');
|
|
const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700';
|
|
|
|
container.innerHTML = `
|
|
<div class="${bgColor} border px-4 py-3 rounded-lg">
|
|
${message}
|
|
</div>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
container.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
|
|
// Load and display reports
|
|
let currentPage = 1;
|
|
let totalPages = 1;
|
|
|
|
async function loadReports(page = 1) {
|
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
|
const tableContainer = document.getElementById('reportsTableContainer');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const tableBody = document.getElementById('reportsTableBody');
|
|
|
|
loadingIndicator.classList.remove('hidden');
|
|
tableContainer.classList.add('hidden');
|
|
emptyState.classList.add('hidden');
|
|
|
|
try {
|
|
const token = getAuthToken();
|
|
if (!token) {
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}?page=${page}&perPage=50`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load reports');
|
|
}
|
|
|
|
const data = await response.json();
|
|
currentPage = data.page;
|
|
totalPages = data.totalPages;
|
|
|
|
loadingIndicator.classList.add('hidden');
|
|
|
|
if (data.items.length === 0) {
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
tableContainer.classList.remove('hidden');
|
|
renderReports(data.items);
|
|
renderPagination();
|
|
} catch (error) {
|
|
console.error('Error loading reports:', error);
|
|
loadingIndicator.classList.add('hidden');
|
|
showMessage('Failed to load reports. Please try again.', 'error');
|
|
}
|
|
}
|
|
|
|
// Render reports table
|
|
function renderReports(reports) {
|
|
const tableBody = document.getElementById('reportsTableBody');
|
|
tableBody.innerHTML = '';
|
|
|
|
reports.forEach(report => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-gray-50';
|
|
|
|
// Get employee name from expanded relation
|
|
let employeeName = '-';
|
|
if (report.expand && report.expand.Employee) {
|
|
const emp = report.expand.Employee;
|
|
employeeName = `${emp.First_Name || ''} ${emp.Last_Name || ''}`.trim() || emp.id || '-';
|
|
} else if (report.Employee) {
|
|
employeeName = report.Employee;
|
|
}
|
|
|
|
// Format report text (strip HTML tags and truncate if too long)
|
|
const reportHtml = report.Report || '-';
|
|
// Strip HTML tags for preview
|
|
const reportText = reportHtml.replace(/<[^>]*>/g, '').trim();
|
|
const reportPreview = reportText.length > 100 ? reportText.substring(0, 100) + '...' : reportText;
|
|
|
|
// Format date and time (convert from UTC to local for display)
|
|
const reportDate = report.Report_Date
|
|
? new Date(report.Report_Date).toLocaleString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
timeZoneName: 'short'
|
|
})
|
|
: '-';
|
|
|
|
// Count and display links (detect Voxer links)
|
|
const links = [];
|
|
const isVoxerLink = (url) => {
|
|
if (!url) return false;
|
|
const lowerUrl = url.toLowerCase();
|
|
return lowerUrl.includes('voxer.com') || lowerUrl.includes('voxer');
|
|
};
|
|
|
|
if (report.Link_01) links.push({ num: 1, url: report.Link_01, isVoxer: isVoxerLink(report.Link_01) });
|
|
if (report.Link_02) links.push({ num: 2, url: report.Link_02, isVoxer: isVoxerLink(report.Link_02) });
|
|
if (report.Link_03) links.push({ num: 3, url: report.Link_03, isVoxer: isVoxerLink(report.Link_03) });
|
|
if (report.Link_04) links.push({ num: 4, url: report.Link_04, isVoxer: isVoxerLink(report.Link_04) });
|
|
if (report.Link_05) links.push({ num: 5, url: report.Link_05, isVoxer: isVoxerLink(report.Link_05) });
|
|
|
|
const linksHtml = links.length > 0
|
|
? links.map(link => {
|
|
if (link.isVoxer) {
|
|
return `<a href="${escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-2 py-1 text-xs font-medium text-white bg-purple-600 hover:bg-purple-700 rounded">🎙️ Voxer ${link.num}</a>`;
|
|
} else {
|
|
return `<a href="${escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-2 py-1 text-xs font-medium text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded border border-indigo-200">🔗 Link ${link.num}</a>`;
|
|
}
|
|
}).join('')
|
|
: '-';
|
|
|
|
row.innerHTML = `
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(employeeName)}</td>
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 text-sm text-gray-900">
|
|
<div class="max-w-xs truncate" title="${escapeHtml(reportText)}">
|
|
${reportPreview !== '-' ? escapeHtml(reportPreview) : '-'}
|
|
</div>
|
|
</td>
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-500">${escapeHtml(report.Centiment || '-')}</td>
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-500">${escapeHtml(reportDate)}</td>
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 text-sm text-gray-500">
|
|
<div class="flex flex-wrap gap-1">
|
|
${linksHtml === '-' ? '-' : linksHtml}
|
|
</div>
|
|
</td>
|
|
<td class="px-3 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button onclick="editReport('${report.id}')" class="text-indigo-600 hover:text-indigo-900 mr-2 sm:mr-4 py-1 px-2 sm:px-0">Edit</button>
|
|
<button onclick="deleteReport('${report.id}')" class="text-red-600 hover:text-red-900 py-1 px-2 sm:px-0">Delete</button>
|
|
</td>
|
|
`;
|
|
tableBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Render pagination
|
|
function renderPagination() {
|
|
const container = document.getElementById('paginationContainer');
|
|
if (totalPages <= 1) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="flex items-center justify-between">';
|
|
html += '<div class="flex-1 flex justify-between sm:hidden">';
|
|
if (currentPage > 1) {
|
|
html += `<button onclick="loadReports(${currentPage - 1})" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Previous</button>`;
|
|
}
|
|
if (currentPage < totalPages) {
|
|
html += `<button onclick="loadReports(${currentPage + 1})" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Next</button>`;
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">';
|
|
html += `<p class="text-sm text-gray-700">Page <span class="font-medium">${currentPage}</span> of <span class="font-medium">${totalPages}</span></p>`;
|
|
html += '<div>';
|
|
if (currentPage > 1) {
|
|
html += `<button onclick="loadReports(${currentPage - 1})" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">Previous</button>`;
|
|
}
|
|
if (currentPage < totalPages) {
|
|
html += `<button onclick="loadReports(${currentPage + 1})" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">Next</button>`;
|
|
}
|
|
html += '</div></div></div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Initialize rich text editor
|
|
function initRichTextEditor() {
|
|
if (!reportEditor) {
|
|
const editorContainer = document.getElementById('reportEditor');
|
|
if (editorContainer && typeof Quill !== 'undefined') {
|
|
try {
|
|
reportEditor = new Quill('#reportEditor', {
|
|
theme: 'snow',
|
|
modules: {
|
|
toolbar: [
|
|
[{ 'header': [1, 2, 3, false] }],
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
|
[{ 'color': [] }, { 'background': [] }],
|
|
['link'],
|
|
['clean']
|
|
]
|
|
}
|
|
});
|
|
|
|
// Sync editor content with hidden textarea for form submission
|
|
reportEditor.on('text-change', function() {
|
|
const htmlContent = reportEditor.root.innerHTML;
|
|
const textarea = document.getElementById('Report');
|
|
if (textarea) {
|
|
textarea.value = htmlContent;
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error initializing rich text editor:', error);
|
|
}
|
|
}
|
|
}
|
|
return reportEditor;
|
|
}
|
|
|
|
// Open modal for add/edit
|
|
async function openModal(reportId = null) {
|
|
const modal = document.getElementById('reportModal');
|
|
const form = document.getElementById('reportForm');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const reportIdInput = document.getElementById('reportId');
|
|
|
|
// Show modal first so editor container is visible
|
|
modal.classList.remove('hidden');
|
|
|
|
// Initialize rich text editor (with a small delay to ensure DOM is ready)
|
|
setTimeout(() => {
|
|
initRichTextEditor();
|
|
}, 50);
|
|
|
|
// Ensure employees are loaded
|
|
if (employeesList.length === 0) {
|
|
await loadEmployees();
|
|
}
|
|
|
|
if (reportId) {
|
|
modalTitle.textContent = 'Edit Employee Report';
|
|
reportIdInput.value = reportId;
|
|
await loadReportForEdit(reportId);
|
|
} else {
|
|
modalTitle.textContent = 'Add Employee Report';
|
|
reportIdInput.value = '';
|
|
form.reset();
|
|
// Clear the rich text editor after it's initialized
|
|
setTimeout(() => {
|
|
if (reportEditor) {
|
|
reportEditor.setContents([]);
|
|
reportEditor.root.innerHTML = '<p><br></p>';
|
|
document.getElementById('Report').value = '';
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// Load report data for editing
|
|
async function loadReportForEdit(id) {
|
|
try {
|
|
const token = getAuthToken();
|
|
const response = await fetch(`${API_BASE}/${id}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load report');
|
|
}
|
|
|
|
const report = await response.json();
|
|
document.getElementById('Employee').value = report.Employee || '';
|
|
|
|
// Set rich text editor content
|
|
const reportContent = report.Report || '';
|
|
if (reportEditor) {
|
|
// Set the HTML content in the editor
|
|
reportEditor.root.innerHTML = reportContent || '<p><br></p>';
|
|
document.getElementById('Report').value = reportContent;
|
|
} else {
|
|
// Editor not initialized yet, try again after a short delay
|
|
setTimeout(() => {
|
|
if (reportEditor) {
|
|
reportEditor.root.innerHTML = reportContent || '<p><br></p>';
|
|
document.getElementById('Report').value = reportContent;
|
|
} else {
|
|
document.getElementById('Report').value = reportContent;
|
|
}
|
|
}, 150);
|
|
}
|
|
|
|
document.getElementById('Centiment').value = report.Centiment || '';
|
|
|
|
// Format date and time for input field (YYYY-MM-DDTHH:mm) - convert from UTC to local
|
|
if (report.Report_Date) {
|
|
const date = new Date(report.Report_Date);
|
|
// Convert UTC to local time for display
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
document.getElementById('Report_Date').value = `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
} else {
|
|
document.getElementById('Report_Date').value = '';
|
|
}
|
|
|
|
document.getElementById('Link_01').value = report.Link_01 || '';
|
|
document.getElementById('Link_02').value = report.Link_02 || '';
|
|
document.getElementById('Link_03').value = report.Link_03 || '';
|
|
document.getElementById('Link_04').value = report.Link_04 || '';
|
|
document.getElementById('Link_05').value = report.Link_05 || '';
|
|
} catch (error) {
|
|
console.error('Error loading report:', error);
|
|
showMessage('Failed to load report data.', 'error');
|
|
}
|
|
}
|
|
|
|
// Edit report
|
|
window.editReport = function(id) {
|
|
openModal(id);
|
|
};
|
|
|
|
// Delete report
|
|
window.deleteReport = async function(id) {
|
|
if (!confirm('Are you sure you want to delete this report?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = getAuthToken();
|
|
const response = await fetch(`${API_BASE}/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to delete report');
|
|
}
|
|
|
|
showMessage('Report deleted successfully.', 'success');
|
|
loadReports(currentPage);
|
|
} catch (error) {
|
|
console.error('Error deleting report:', error);
|
|
showMessage('Failed to delete report.', 'error');
|
|
}
|
|
};
|
|
|
|
// Initialize page - check auth and display user info
|
|
async function initPage() {
|
|
// First, ensure we have the token loaded from localStorage if not in auth store
|
|
const token = getAuthToken();
|
|
|
|
if (!token) {
|
|
// No token at all, redirect to login
|
|
console.log('No auth token found, redirecting to login');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
|
|
// Check if auth store is valid, if not try to refresh
|
|
if (!pb.authStore.isValid) {
|
|
try {
|
|
// Try to refresh the token
|
|
await pb.collection('users').authRefresh();
|
|
// Update localStorage after refresh
|
|
if (pb.authStore.token && pb.authStore.model) {
|
|
localStorage.setItem('pb_auth_token', pb.authStore.token);
|
|
localStorage.setItem('pb_auth_model', JSON.stringify(pb.authStore.model));
|
|
}
|
|
} catch (e) {
|
|
console.error('Token refresh failed:', e);
|
|
// If refresh fails but we have a token, still try to continue
|
|
// The API will validate it
|
|
if (!pb.authStore.token) {
|
|
pb.authStore.clear();
|
|
localStorage.removeItem('pb_auth_token');
|
|
localStorage.removeItem('pb_auth_model');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display user information
|
|
const user = pb.authStore.model;
|
|
if (user) {
|
|
const userEmailElement = document.getElementById('userEmail');
|
|
if (userEmailElement) {
|
|
userEmailElement.textContent = user.email || 'User';
|
|
}
|
|
} else {
|
|
// Try to get user from localStorage
|
|
const storedModel = localStorage.getItem('pb_auth_model');
|
|
if (storedModel) {
|
|
try {
|
|
const userData = JSON.parse(storedModel);
|
|
const userEmailElement = document.getElementById('userEmail');
|
|
if (userEmailElement) {
|
|
userEmailElement.textContent = userData.email || 'User';
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing stored user model:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle logout button
|
|
const logoutButton = document.getElementById('logoutButton');
|
|
if (logoutButton) {
|
|
logoutButton.addEventListener('click', async () => {
|
|
try {
|
|
await pb.authStore.clear();
|
|
localStorage.removeItem('pb_auth_token');
|
|
localStorage.removeItem('pb_auth_model');
|
|
window.location.href = '/index.html';
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
pb.authStore.clear();
|
|
localStorage.removeItem('pb_auth_token');
|
|
localStorage.removeItem('pb_auth_model');
|
|
window.location.href = '/index.html';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle form submission
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Initialize page (auth check, user display, logout)
|
|
await initPage();
|
|
|
|
const form = document.getElementById('reportForm');
|
|
const modal = document.getElementById('reportModal');
|
|
const closeModal = document.getElementById('closeModal');
|
|
const cancelButton = document.getElementById('cancelButton');
|
|
const addReportButton = document.getElementById('addReportButton');
|
|
const addReportButtonEmpty = document.getElementById('addReportButtonEmpty');
|
|
|
|
// Load reports on page load
|
|
loadReports();
|
|
|
|
// Open modal handlers
|
|
if (addReportButton) {
|
|
addReportButton.addEventListener('click', () => openModal());
|
|
}
|
|
if (addReportButtonEmpty) {
|
|
addReportButtonEmpty.addEventListener('click', () => openModal());
|
|
}
|
|
|
|
// Close modal handlers
|
|
if (closeModal) {
|
|
closeModal.addEventListener('click', () => {
|
|
modal.classList.add('hidden');
|
|
});
|
|
}
|
|
if (cancelButton) {
|
|
cancelButton.addEventListener('click', () => {
|
|
modal.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
// Form submission
|
|
if (form) {
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const reportId = document.getElementById('reportId').value;
|
|
|
|
// Convert datetime-local to UTC ISO string
|
|
let reportDateUTC = undefined;
|
|
const reportDateValue = document.getElementById('Report_Date').value;
|
|
if (reportDateValue) {
|
|
const localDate = new Date(reportDateValue);
|
|
reportDateUTC = localDate.toISOString();
|
|
}
|
|
|
|
const formData = {
|
|
Employee: document.getElementById('Employee').value || undefined,
|
|
Report: document.getElementById('Report').value || undefined,
|
|
Centiment: document.getElementById('Centiment').value || undefined,
|
|
Report_Date: reportDateUTC,
|
|
Link_01: document.getElementById('Link_01').value || undefined,
|
|
Link_02: document.getElementById('Link_02').value || undefined,
|
|
Link_03: document.getElementById('Link_03').value || undefined,
|
|
Link_04: document.getElementById('Link_04').value || undefined,
|
|
Link_05: document.getElementById('Link_05').value || undefined,
|
|
};
|
|
|
|
try {
|
|
const token = getAuthToken();
|
|
const url = reportId ? `${API_BASE}/${reportId}` : API_BASE;
|
|
const method = reportId ? 'PATCH' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to save report');
|
|
}
|
|
|
|
showMessage(`Report ${reportId ? 'updated' : 'created'} successfully.`, 'success');
|
|
modal.classList.add('hidden');
|
|
loadReports(reportId ? currentPage : 1);
|
|
} catch (error) {
|
|
console.error('Error saving report:', error);
|
|
showMessage(error.message || 'Failed to save report.', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load employees when page loads
|
|
loadEmployees();
|
|
});
|
|
|
|
// Utility function to escape HTML
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|