543 lines
23 KiB
JavaScript
543 lines
23 KiB
JavaScript
// Employee CRUD operations
|
|
|
|
let currentPage = 1;
|
|
let itemsPerPage = 20;
|
|
let allEmployees = [];
|
|
let employeesCollectionName = 'Employee_Records';
|
|
let currentView = localStorage.getItem('employeesView') || 'table'; // 'table' or 'card'
|
|
|
|
// Common employee field names that we'll display (matching PocketBase schema)
|
|
const standardFields = ['First_Name', 'Last_Name', 'Email_Address', 'Phone_Number', 'Voxer_ID', 'Status'];
|
|
|
|
/**
|
|
* Load all employees with optional search filter
|
|
*/
|
|
async function loadEmployees(page = 1) {
|
|
try {
|
|
// Show loading state based on current view
|
|
if (currentView === 'table') {
|
|
const tbody = document.getElementById('employeesTableBody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr id="loadingRow"><td colspan="7" class="px-6 py-8 text-center text-gray-500">Loading...</td></tr>';
|
|
}
|
|
} else {
|
|
const container = document.getElementById('employeesCardContainer');
|
|
if (container) {
|
|
container.innerHTML = '<div class="text-center py-8 text-gray-500">Loading...</div>';
|
|
}
|
|
}
|
|
|
|
currentPage = page;
|
|
const searchTerm = document.getElementById('searchInput')?.value || '';
|
|
|
|
// Build search filter
|
|
let searchFilter = '';
|
|
if (searchTerm) {
|
|
// Escape special characters for PocketBase filter
|
|
const escapedTerm = searchTerm.replace(/"/g, '\\"');
|
|
// Search across common text fields
|
|
searchFilter = `First_Name ~ "${escapedTerm}" || Last_Name ~ "${escapedTerm}" || Email_Address ~ "${escapedTerm}" || Phone_Number ~ "${escapedTerm}" || Voxer_ID ~ "${escapedTerm}" || Status ~ "${escapedTerm}"`;
|
|
}
|
|
|
|
const result = await pb.collection(employeesCollectionName).getList(page, itemsPerPage, {
|
|
filter: searchFilter || undefined,
|
|
sort: 'Last_Name',
|
|
});
|
|
|
|
allEmployees = result.items;
|
|
displayEmployees(allEmployees);
|
|
displayPagination(result.page, result.totalPages, result.totalItems);
|
|
|
|
// Update view based on current view preference
|
|
updateViewDisplay();
|
|
} catch (error) {
|
|
console.error('Error loading employees:', error);
|
|
showMessage('Error loading employees: ' + (error.message || 'Unknown error'), 'error');
|
|
if (currentView === 'table') {
|
|
const tbody = document.getElementById('employeesTableBody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-8 text-center text-red-500">Error loading employees. Please refresh the page.</td></tr>';
|
|
}
|
|
} else {
|
|
const container = document.getElementById('employeesCardContainer');
|
|
if (container) {
|
|
container.innerHTML = '<div class="text-center py-8 text-red-500">Error loading employees. Please refresh the page.</div>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display employees in table or card view
|
|
*/
|
|
function displayEmployees(employees) {
|
|
if (currentView === 'table') {
|
|
displayEmployeesTable(employees);
|
|
} else {
|
|
displayEmployeesCards(employees);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display employees in table view
|
|
*/
|
|
function displayEmployeesTable(employees) {
|
|
const tbody = document.getElementById('employeesTableBody');
|
|
if (!tbody) return;
|
|
|
|
if (!employees || employees.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-8 text-center text-gray-500">No employees found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = employees.map(employee => {
|
|
// Use correct field names from schema
|
|
const firstName = employee.First_Name || '-';
|
|
const lastName = employee.Last_Name || '-';
|
|
const email = employee.Email_Address || '-';
|
|
const phone = employee.Phone_Number || '-';
|
|
const voxerId = employee.Voxer_ID || '-';
|
|
const status = employee.Status || '-';
|
|
|
|
return `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(firstName)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(lastName)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(phone)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(email)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(voxerId)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(status)}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button onclick="openEmployeeModal('${employee.id}')" class="bg-green-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition">Edit</button>
|
|
<button onclick="event.stopPropagation(); deleteEmployee('${employee.id}')" class="bg-red-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Display employees in card view
|
|
*/
|
|
function displayEmployeesCards(employees) {
|
|
const container = document.getElementById('employeesCardContainer');
|
|
if (!container) return;
|
|
|
|
if (!employees || employees.length === 0) {
|
|
container.innerHTML = '<div class="text-center py-8 text-gray-500">No employees found</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = employees.map(employee => {
|
|
// Use correct field names from schema
|
|
const firstName = employee.First_Name || '';
|
|
const lastName = employee.Last_Name || '';
|
|
const fullName = `${firstName} ${lastName}`.trim() || 'N/A';
|
|
const email = employee.Email_Address || '-';
|
|
const phone = employee.Phone_Number || '-';
|
|
const voxerId = employee.Voxer_ID || '-';
|
|
const status = employee.Status || '-';
|
|
|
|
return `
|
|
<div
|
|
onclick="openEmployeeModal('${employee.id}')"
|
|
class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:shadow-2xl hover:border-white/50 hover:bg-white/70 cursor-pointer transition-all duration-300 hover:scale-[1.02]"
|
|
>
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<h3 class="text-xl font-semibold text-gray-900">${escapeHtml(fullName)}</h3>
|
|
${status !== '-' ? `
|
|
<div class="mt-2">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
|
${escapeHtml(status)}
|
|
</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="event.stopPropagation(); openEmployeeModal('${employee.id}')" class="bg-green-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition">Edit</button>
|
|
<button onclick="event.stopPropagation(); deleteEmployee('${employee.id}')" class="bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition">Delete</button>
|
|
<svg class="w-5 h-5 text-gray-400 flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Email</span>
|
|
${email !== '-' ? `
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(email)}</p>
|
|
<a href="mailto:${escapeHtml(email)}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">Send Email</a>
|
|
` : '<p class="text-sm font-medium text-gray-900 mt-1">-</p>'}
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Phone</span>
|
|
${phone !== '-' ? `
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(phone)}</p>
|
|
<a href="tel:${phone.replace(/[^\d+]/g, '')}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">Call</a>
|
|
` : '<p class="text-sm font-medium text-gray-900 mt-1">-</p>'}
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Voxer ID</span>
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(voxerId)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Display pagination controls
|
|
*/
|
|
function displayPagination(currentPage, totalPages, totalItems) {
|
|
const container = document.getElementById('paginationContainer');
|
|
if (!container) return;
|
|
|
|
if (totalPages <= 1) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="flex items-center gap-2">';
|
|
|
|
// Previous button
|
|
if (currentPage > 1) {
|
|
html += `<button onclick="loadEmployees(${currentPage - 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Previous</button>`;
|
|
}
|
|
|
|
// Page info
|
|
html += `<span class="text-sm text-gray-700">Page ${currentPage} of ${totalPages} (${totalItems} total)</span>`;
|
|
|
|
// Next button
|
|
if (currentPage < totalPages) {
|
|
html += `<button onclick="loadEmployees(${currentPage + 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Next</button>`;
|
|
}
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Load employee for editing
|
|
*/
|
|
async function loadEmployeeForEdit(employeeId) {
|
|
try {
|
|
if (!employeeId) {
|
|
showMessage('No employee ID provided', 'error');
|
|
return null;
|
|
}
|
|
|
|
const employee = await pb.collection(employeesCollectionName).getOne(employeeId);
|
|
|
|
// Set current employee ID for reports
|
|
window.currentEmployeeId = employeeId;
|
|
|
|
// Update modal header
|
|
if (typeof updateEmployeeModalInfo === 'function') {
|
|
updateEmployeeModalInfo(employee);
|
|
}
|
|
|
|
// Show reports tab for existing employees - do this after a short delay to ensure DOM is ready
|
|
setTimeout(() => {
|
|
const reportsTabButton = document.getElementById('tab-reports');
|
|
if (reportsTabButton) {
|
|
reportsTabButton.classList.remove('hidden');
|
|
}
|
|
}, 50);
|
|
|
|
// Populate standard form fields using correct field names
|
|
const firstNameField = document.getElementById('First_Name');
|
|
if (firstNameField) firstNameField.value = employee.First_Name || '';
|
|
|
|
const lastNameField = document.getElementById('Last_Name');
|
|
if (lastNameField) lastNameField.value = employee.Last_Name || '';
|
|
|
|
const emailField = document.getElementById('Email_Address');
|
|
if (emailField) emailField.value = employee.Email_Address || '';
|
|
|
|
const phoneField = document.getElementById('Phone_Number');
|
|
if (phoneField) phoneField.value = employee.Phone_Number || '';
|
|
|
|
const voxerField = document.getElementById('Voxer_ID');
|
|
if (voxerField) voxerField.value = employee.Voxer_ID || '';
|
|
|
|
const statusField = document.getElementById('Status');
|
|
if (statusField) statusField.value = employee.Status || '';
|
|
|
|
// Handle additional fields dynamically
|
|
const additionalFieldsContainer = document.getElementById('additionalFieldsContainer');
|
|
if (additionalFieldsContainer) {
|
|
additionalFieldsContainer.innerHTML = '';
|
|
|
|
// Get all fields from the employee record
|
|
const allFields = Object.keys(employee);
|
|
const systemFields = new Set(['id', 'created', 'updated', 'collectionId', 'collectionName']);
|
|
|
|
// Add any fields that aren't in the standard fields
|
|
allFields.forEach(field => {
|
|
// Skip system fields
|
|
if (systemFields.has(field) || field.startsWith('@')) {
|
|
return;
|
|
}
|
|
|
|
// Skip standard fields (already handled above)
|
|
if (standardFields.includes(field)) {
|
|
return;
|
|
}
|
|
|
|
// Create input for this additional field
|
|
if (employee[field] !== undefined) {
|
|
const fieldLabel = field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
const fieldId = `field_${field}`;
|
|
|
|
const fieldDiv = document.createElement('div');
|
|
fieldDiv.innerHTML = `
|
|
<label for="${fieldId}" class="block text-sm font-medium text-gray-700 mb-1">${fieldLabel}</label>
|
|
<input type="text" id="${fieldId}" name="${field}" value="${escapeHtml(String(employee[field] || ''))}" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
|
`;
|
|
additionalFieldsContainer.appendChild(fieldDiv.firstElementChild);
|
|
}
|
|
});
|
|
}
|
|
|
|
return employee;
|
|
} catch (error) {
|
|
console.error('Error loading employee:', error);
|
|
showMessage('Error loading employee: ' + (error.message || 'Unknown error'), 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, m => map[m]);
|
|
}
|
|
|
|
/**
|
|
* Save employee (create or update)
|
|
*/
|
|
async function saveEmployee() {
|
|
try {
|
|
const form = document.getElementById('employeeForm');
|
|
if (!form) {
|
|
showMessage('Form not found', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(form);
|
|
const employeeId = document.getElementById('employeeId')?.value;
|
|
|
|
// Build employee object
|
|
const employeeData = {};
|
|
|
|
// Get all form fields from FormData (this includes standard fields)
|
|
for (const [key, value] of formData.entries()) {
|
|
if (key === 'id') continue;
|
|
// Include the value (even if empty for standard fields to allow clearing)
|
|
if (value !== null && value !== undefined) {
|
|
employeeData[key] = value.trim();
|
|
}
|
|
}
|
|
|
|
// Also get additional fields from dynamically created inputs
|
|
const additionalFields = document.querySelectorAll('#additionalFieldsContainer input');
|
|
additionalFields.forEach(input => {
|
|
if (input.name && input.value !== null && input.value !== undefined) {
|
|
employeeData[input.name] = input.value.trim();
|
|
}
|
|
});
|
|
|
|
if (employeeId) {
|
|
// Update existing employee
|
|
const updatedEmployee = await pb.collection(employeesCollectionName).update(employeeId, employeeData);
|
|
const employeeName = `${updatedEmployee.First_Name || ''} ${updatedEmployee.Last_Name || ''}`.trim() || 'Employee';
|
|
showMessage(`${employeeName} updated successfully`, 'success');
|
|
} else {
|
|
// Create new employee
|
|
const newEmployee = await pb.collection(employeesCollectionName).create(employeeData);
|
|
const employeeName = `${newEmployee.First_Name || ''} ${newEmployee.Last_Name || ''}`.trim() || 'Employee';
|
|
showMessage(`${employeeName} created successfully`, 'success');
|
|
}
|
|
|
|
if (typeof closeEmployeeModal === 'function') {
|
|
closeEmployeeModal();
|
|
}
|
|
loadEmployees(currentPage);
|
|
} catch (error) {
|
|
console.error('Error saving employee:', error);
|
|
const errorMsg = error.message || 'Unknown error';
|
|
showMessage('Error saving employee: ' + errorMsg, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete employee
|
|
*/
|
|
async function deleteEmployee(employeeId) {
|
|
// Find the employee name for the confirmation message
|
|
const employee = allEmployees.find(emp => emp.id === employeeId);
|
|
const employeeName = employee
|
|
? `${employee.First_Name || ''} ${employee.Last_Name || ''}`.trim() || 'this employee'
|
|
: 'this employee';
|
|
|
|
if (!confirm(`Are you sure you want to delete ${employeeName}? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await pb.collection(employeesCollectionName).delete(employeeId);
|
|
showMessage(`Employee ${employeeName} deleted successfully`, 'success');
|
|
// Reload employees with current view state
|
|
loadEmployees(currentPage);
|
|
} catch (error) {
|
|
console.error('Error deleting employee:', error);
|
|
showMessage('Error deleting employee: ' + (error.message || 'Unknown error'), 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show message to user
|
|
*/
|
|
function showMessage(message, type = 'success') {
|
|
const container = document.getElementById('messageContainer');
|
|
if (!container) return;
|
|
|
|
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 text-sm">
|
|
${message}
|
|
</div>
|
|
`;
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
container.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Switch between table and card view
|
|
*/
|
|
function switchView(view) {
|
|
currentView = view;
|
|
localStorage.setItem('employeesView', view);
|
|
updateViewDisplay();
|
|
displayEmployees(allEmployees);
|
|
}
|
|
|
|
/**
|
|
* Update view display based on current view preference
|
|
*/
|
|
function updateViewDisplay() {
|
|
const tableView = document.getElementById('tableView');
|
|
const cardView = document.getElementById('cardView');
|
|
const tableViewBtn = document.getElementById('tableViewBtn');
|
|
const cardViewBtn = document.getElementById('cardViewBtn');
|
|
|
|
if (currentView === 'table') {
|
|
// Show table, hide card
|
|
if (tableView) tableView.classList.remove('hidden');
|
|
if (cardView) cardView.classList.add('hidden');
|
|
|
|
// Table button: active (indigo with shadow)
|
|
if (tableViewBtn) {
|
|
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
|
|
tableViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
|
}
|
|
// Card button: inactive (transparent)
|
|
if (cardViewBtn) {
|
|
cardViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
|
cardViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
|
|
}
|
|
} else {
|
|
// Show card, hide table
|
|
if (tableView) tableView.classList.add('hidden');
|
|
if (cardView) cardView.classList.remove('hidden');
|
|
|
|
// Table button: inactive (transparent)
|
|
if (tableViewBtn) {
|
|
tableViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
|
tableViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
|
|
}
|
|
// Card button: active (indigo with shadow)
|
|
if (cardViewBtn) {
|
|
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
|
|
cardViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Switch employee modal tab
|
|
*/
|
|
function switchEmployeeTab(tabName) {
|
|
// Hide all tab contents
|
|
document.querySelectorAll('.employee-tab-content').forEach(tab => {
|
|
tab.classList.add('hidden');
|
|
});
|
|
|
|
// Remove active state from all tabs
|
|
document.querySelectorAll('.employee-tab').forEach(btn => {
|
|
btn.classList.remove('border-indigo-500', 'text-indigo-600');
|
|
btn.classList.add('border-transparent', 'text-gray-500');
|
|
});
|
|
|
|
// Show selected tab content
|
|
const selectedTab = document.getElementById(`employee-tab-${tabName}`);
|
|
if (selectedTab) {
|
|
selectedTab.classList.remove('hidden');
|
|
}
|
|
|
|
// Activate selected tab button
|
|
const selectedButton = document.getElementById(`tab-${tabName}`);
|
|
if (selectedButton) {
|
|
selectedButton.classList.add('border-indigo-500', 'text-indigo-600');
|
|
selectedButton.classList.remove('border-transparent', 'text-gray-500');
|
|
}
|
|
|
|
// Load reports if reports tab is selected
|
|
if (tabName === 'reports' && window.currentEmployeeId && typeof loadEmployeeReports === 'function') {
|
|
setTimeout(() => {
|
|
loadEmployeeReports(window.currentEmployeeId);
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update employee modal header info
|
|
*/
|
|
function updateEmployeeModalInfo(employee) {
|
|
const firstName = employee.First_Name || '';
|
|
const lastName = employee.Last_Name || '';
|
|
const fullName = `${firstName} ${lastName}`.trim() || 'Employee';
|
|
document.getElementById('employeeModalTitle').textContent = fullName;
|
|
}
|
|
|
|
// Make functions globally accessible
|
|
window.loadEmployees = loadEmployees;
|
|
window.loadEmployeeForEdit = loadEmployeeForEdit;
|
|
window.saveEmployee = saveEmployee;
|
|
window.deleteEmployee = deleteEmployee;
|
|
window.switchView = switchView;
|
|
window.updateViewDisplay = updateViewDisplay;
|
|
window.switchEmployeeTab = switchEmployeeTab;
|