From d4f244609a7d15d3f477200f6c55c3f06edb19f6 Mon Sep 17 00:00:00 2001 From: eewing Date: Wed, 17 Dec 2025 15:58:04 -0600 Subject: [PATCH] adding employee page. needs reports --- dashboard.html | 16 +- employees.html | 445 +++++++++++++++++++++++++++++++++ js/employee-reports.js | 264 ++++++++++++++++++++ js/employees.js | 542 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1266 insertions(+), 1 deletion(-) create mode 100644 employees.html create mode 100644 js/employee-reports.js create mode 100644 js/employees.js diff --git a/dashboard.html b/dashboard.html index 2bfffe1..2dd13d9 100644 --- a/dashboard.html +++ b/dashboard.html @@ -57,7 +57,7 @@ -
+
@@ -85,6 +85,20 @@

Manage job notes and reports

View Notes →
+ + +
+
+
+ + + +
+
+

Employees

+

Manage employee records

+ View Employees → +
diff --git a/employees.html b/employees.html new file mode 100644 index 0000000..639bd75 --- /dev/null +++ b/employees.html @@ -0,0 +1,445 @@ + + + + + + Job Tracking - Employees + + + + + + + + +
+ +
+
+

Employees

+

Manage employee records

+
+ +
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
First NameLast NamePhoneEmailVoxer IDStatusActions
Loading...
+
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + diff --git a/js/employee-reports.js b/js/employee-reports.js new file mode 100644 index 0000000..55d9641 --- /dev/null +++ b/js/employee-reports.js @@ -0,0 +1,264 @@ +// Employee Reports CRUD operations + +const reportsCollectionName = 'Employee_Reports'; +window.currentEmployeeId = null; + +/** + * Load reports for a specific employee + */ +async function loadEmployeeReports(employeeId) { + try { + if (!employeeId) { + document.getElementById('reportsList').innerHTML = '

No employee selected

'; + return; + } + + window.currentEmployeeId = employeeId; + + // Filter reports by id matching the employee id (Employee_Records id) + // The id field in Employee_Reports links to Employee_Records id + const result = await pb.collection(reportsCollectionName).getList(1, 100, { + filter: `id = "${employeeId}"`, + sort: '-entered_date', + }); + + displayReports(result.items); + } catch (error) { + console.error('Error loading reports:', error); + document.getElementById('reportsList').innerHTML = '

Error loading reports. Please try again.

'; + } +} + +/** + * Display reports in the reports list + */ +function displayReports(reports) { + const container = document.getElementById('reportsList'); + if (!container) return; + + if (!reports || reports.length === 0) { + container.innerHTML = '

No reports found. Click "Add Report" to create one.

'; + return; + } + + container.innerHTML = reports.map(report => { + const enteredDate = report.entered_date + ? new Date(report.entered_date).toLocaleDateString() + : '-'; + + return ` +
+
+
+
+ ${report.entered_date ? ` + ${escapeHtml(enteredDate)} + ` : ''} + ${report.entered_by ? ` + Entered by: ${escapeHtml(report.entered_by)} + ` : ''} + ${report.row ? ` + Row: ${escapeHtml(report.row)} + ` : ''} +
+ ${report.report ? ` +

${escapeHtml(report.report)}

+ ` : '

No report content

'} + ${report.id_number ? ` +

ID Number: ${escapeHtml(report.id_number)}

+ ` : ''} +
+
+ + +
+
+
+ `; + }).join(''); +} + +/** + * Open report modal for creating or editing + */ +async function openReportModal(reportId = null) { + document.getElementById('reportModal').classList.remove('hidden'); + + if (reportId) { + document.getElementById('reportModalTitle').textContent = 'Edit Report'; + document.getElementById('reportId').value = reportId; + await loadReportForEdit(reportId); + } else { + document.getElementById('reportModalTitle').textContent = 'Add Report'; + document.getElementById('reportForm').reset(); + document.getElementById('reportId').value = ''; + // Set the employee id for linking + document.getElementById('reportEmployeeId').value = window.currentEmployeeId || ''; + } +} + +/** + * Close report modal + */ +function closeReportModal() { + document.getElementById('reportModal').classList.add('hidden'); + document.getElementById('reportForm').reset(); + document.getElementById('reportId').value = ''; +} + +/** + * Load report for editing + */ +async function loadReportForEdit(reportId) { + try { + const report = await pb.collection(reportsCollectionName).getOne(reportId); + + // Populate form fields + document.getElementById('reportRow').value = report.row || ''; + document.getElementById('reportEnteredBy').value = report.entered_by || ''; + document.getElementById('reportEnteredDate').value = report.entered_date + ? new Date(report.entered_date).toISOString().split('T')[0] + : ''; + document.getElementById('reportIdNumber').value = report.id_number || ''; + document.getElementById('reportContent').value = report.report || ''; + document.getElementById('reportEmployeeId').value = report.id || window.currentEmployeeId || ''; + + return report; + } catch (error) { + console.error('Error loading report:', error); + showMessage('Error loading report: ' + (error.message || 'Unknown error'), 'error'); + return null; + } +} + +/** + * Save report (create or update) + */ +async function saveReport() { + try { + const form = document.getElementById('reportForm'); + if (!form) { + showMessage('Form not found', 'error'); + return; + } + + const formData = new FormData(form); + const reportId = document.getElementById('reportId')?.value; + const employeeId = window.currentEmployeeId || document.getElementById('reportEmployeeId')?.value; + + if (!employeeId) { + showMessage('No employee ID found. Please save the employee first.', 'error'); + return; + } + + // Build report object + // Note: In PocketBase, when creating, we can't set the id field directly + // So we'll use the id field value to link to Employee_Records + // But we need to check if this is actually a relation field or a text field + // For now, we'll set it as a field value + const reportData = {}; + + // Set the id field to link to Employee_Records (this is the linking field) + reportData.id = employeeId; + + // Get all form fields + for (const [key, value] of formData.entries()) { + if (key === 'id') continue; // Skip the hidden id field + + if (value !== null && value !== undefined) { + if (key === 'entered_date' && value) { + // Convert date to ISO string + reportData[key] = new Date(value).toISOString(); + } else { + reportData[key] = value.trim(); + } + } + } + + // Get employee info to populate name fields if available + if (employeeId && typeof loadEmployeeForEdit === 'undefined') { + // Try to get employee data if we have access to it + try { + const employee = await pb.collection('Employee_Records').getOne(employeeId); + if (employee) { + reportData.first_name = employee.First_Name || ''; + reportData.last_name = employee.Last_Name || ''; + reportData.combined_name = `${employee.First_Name || ''} ${employee.Last_Name || ''}`.trim(); + } + } catch (e) { + console.warn('Could not load employee data for report:', e); + } + } + + if (reportId) { + // Update existing report + await pb.collection(reportsCollectionName).update(reportId, reportData); + showMessage('Report updated successfully', 'success'); + } else { + // Create new report + await pb.collection(reportsCollectionName).create(reportData); + showMessage('Report created successfully', 'success'); + } + + closeReportModal(); + if (window.currentEmployeeId) { + loadEmployeeReports(window.currentEmployeeId); + } + } catch (error) { + console.error('Error saving report:', error); + const errorMsg = error.message || 'Unknown error'; + showMessage('Error saving report: ' + errorMsg, 'error'); + } +} + +/** + * Delete report + */ +async function deleteReport(reportId) { + if (!confirm('Are you sure you want to delete this report? This action cannot be undone.')) { + return; + } + + try { + await pb.collection(reportsCollectionName).delete(reportId); + showMessage('Report deleted successfully', 'success'); + if (window.currentEmployeeId) { + loadEmployeeReports(window.currentEmployeeId); + } + } catch (error) { + console.error('Error deleting report:', error); + showMessage('Error deleting report: ' + (error.message || 'Unknown error'), 'error'); + } +} + +/** + * Escape HTML to prevent XSS + */ +function escapeHtml(text) { + if (text == null) return ''; + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return String(text).replace(/[&<>"']/g, m => map[m]); +} + +// Make functions globally accessible +window.loadEmployeeReports = loadEmployeeReports; +window.openReportModal = openReportModal; +window.closeReportModal = closeReportModal; +window.saveReport = saveReport; +window.deleteReport = deleteReport; diff --git a/js/employees.js b/js/employees.js new file mode 100644 index 0000000..aa908e1 --- /dev/null +++ b/js/employees.js @@ -0,0 +1,542 @@ +// 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 = 'Loading...'; + } + } else { + const container = document.getElementById('employeesCardContainer'); + if (container) { + container.innerHTML = '
Loading...
'; + } + } + + 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 = 'Error loading employees. Please refresh the page.'; + } + } else { + const container = document.getElementById('employeesCardContainer'); + if (container) { + container.innerHTML = '
Error loading employees. Please refresh the page.
'; + } + } + } +} + +/** + * 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 = 'No employees found'; + 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 ` + + ${escapeHtml(firstName)} + ${escapeHtml(lastName)} + ${escapeHtml(phone)} + ${escapeHtml(email)} + ${escapeHtml(voxerId)} + ${escapeHtml(status)} + +
+ + +
+ + + `; + }).join(''); +} + +/** + * Display employees in card view + */ +function displayEmployeesCards(employees) { + const container = document.getElementById('employeesCardContainer'); + if (!container) return; + + if (!employees || employees.length === 0) { + container.innerHTML = '
No employees found
'; + 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 ` +
+
+
+
+

${escapeHtml(fullName)}

+ ${status !== '-' ? ` +
+ + ${escapeHtml(status)} + +
+ ` : ''} +
+
+ + + + + +
+
+
+ +
+
+ Email + ${email !== '-' ? ` +

${escapeHtml(email)}

+ Send Email + ` : '

-

'} +
+ +
+ Phone + ${phone !== '-' ? ` +

${escapeHtml(phone)}

+ Call + ` : '

-

'} +
+ +
+ Voxer ID +

${escapeHtml(voxerId)}

+
+
+
+ `; + }).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 = '
'; + + // Previous button + if (currentPage > 1) { + html += ``; + } + + // Page info + html += `Page ${currentPage} of ${totalPages} (${totalItems} total)`; + + // Next button + if (currentPage < totalPages) { + html += ``; + } + + html += '
'; + 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 = ` + + + `; + 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 = ` +
+ ${message} +
+ `; + + // 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;