diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b763c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ +bun.lockb + +# Environment variables +.env +.env.local + +# Build outputs +dist/ +build/ +*.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + diff --git a/README.md b/README.md index 0621f68..6e7d0ac 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ -# Employee_records +# Employee Records +Employee records management system built with Hono, Bun, and PocketBase. + +## Features + +- 🔐 Authentication via PocketBase +- 📊 Dashboard with employee records and reports management +- 🎨 Modern UI with Tailwind CSS +- ⚡ Fast server with Hono and Bun + +## Tech Stack + +- **Backend**: Hono (web framework) + Bun (runtime) +- **Frontend**: Vanilla JavaScript + Tailwind CSS +- **Database/Auth**: PocketBase (https://pocketbase.ccllc.pro) + +## Prerequisites + +- [Bun](https://bun.sh) installed on your system + +## Setup + +1. Install dependencies: +```bash +bun install +``` + +2. Set environment variables (optional): +```bash +export POCKETBASE_URL=https://pocketbase.ccllc.pro +export PORT=3000 +``` + +3. Start the development server: +```bash +bun run dev +``` + +The server will start on `http://localhost:3000` + +## Project Structure + +``` +Employee_records/ +├── src/ +│ ├── server.ts # Main Hono server entry point +│ ├── routes/ +│ │ ├── auth.ts # Authentication routes +│ │ └── api.ts # API routes +│ └── middleware/ +│ └── auth.ts # Authentication middleware +├── public/ +│ ├── index.html # Login page +│ ├── dashboard.html # Dashboard page +│ └── js/ +│ ├── auth.js # Authentication client logic +│ └── dashboard.js # Dashboard logic +├── package.json +└── tsconfig.json +``` + +## PocketBase Setup + +Before using the application, ensure your PocketBase instance at `https://pocketbase.ccllc.pro` has: + +1. A `users` collection (or the default auth collection) +2. User accounts created for login + +The application will connect to this PocketBase instance for authentication. + +## Collections + +The following collections will be set up later: +- `employee_records` - Employee information +- `employee_reports` - Employee reports + +## Development + +- Run the server: `bun run dev` +- Type check: `bun run type-check` + +## License + +See LICENSE file for details. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..e21170f --- /dev/null +++ b/bun.lock @@ -0,0 +1,32 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "employee-records", + "dependencies": { + "hono": "^4.0.0", + "pocketbase": "^0.21.0", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + + "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], + + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + + "hono": ["hono@4.11.0", "", {}, "sha512-Jg8uZzN2ul8/qlyid5FO8O624F3AK0wKtkgoeEON1qBum1rM1itYBxoMKu/1SPJC7F1+xlIZsJMmc4HHhJ1AWg=="], + + "pocketbase": ["pocketbase@0.21.5", "", {}, "sha512-bnI/uinnQps+ElSlzxkc4yvwuSFfKcoszDtXH/4QT2FhGq2mJVUvDlxn+rjRXVntUjPfmMG5LEPZ1eGqV6ssog=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e50fc52 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "employee-records", + "version": "1.0.0", + "description": "Employee records management system with PocketBase", + "type": "module", + "scripts": { + "dev": "bun run src/server.ts", + "start": "bun run src/server.ts", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "hono": "^4.0.0", + "pocketbase": "^0.21.0" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0" + } +} + diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..9441b45 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,133 @@ + + + + + + Employee Records - Dashboard + + + + + + + + +
+
+

Welcome, User

+

Manage your employee records and reports

+
+ + +
+ +
+
+

Employee Records

+
+ + + +
+
+

View and manage employee information

+ + View Employees → + +
+ + +
+
+

Employee Reports

+
+ + + +
+
+

Generate and view employee reports

+ + View Reports → + +
+
+ + +
+

Quick Stats

+
+
+
+ ... +
+
Total Employees
+
+
+
+ ... +
+
Reports Generated
+
+
+
+ ... +
+
Active Employees
+
+
+
+ + +
+

Quick Actions

+ +
+
+ + + + + diff --git a/public/employees.html b/public/employees.html new file mode 100644 index 0000000..799ba1c --- /dev/null +++ b/public/employees.html @@ -0,0 +1,329 @@ + + + + + + Employee Records - Employees + + + + + + + + + + + +
+
+
+

Employee Records

+

Manage employee information

+
+ +
+ + +
+ + +
+
+ +
+ +
+ + +
+ + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + +
IDFirst NameLast NameTypeActions
Loading...
+
+
+ + + +
+ + + + + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..67540bc --- /dev/null +++ b/public/index.html @@ -0,0 +1,63 @@ + + + + + + Employee Records - Login + + + + +
+
+
+

Employee Records

+

Sign in to your account

+
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+ + + + + diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..d8d937d --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,74 @@ +const POCKETBASE_URL = 'https://pocketbase.ccllc.pro'; + +// Initialize PocketBase client +const pb = new PocketBase(POCKETBASE_URL); + +// Load existing auth from cookies (PocketBase stores auth in cookies automatically) +try { + pb.authStore.loadFromCookie(document.cookie); +} catch (e) { + // If cookie loading fails, try localStorage + 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); + } + } +} + +// Check if user is already authenticated +function checkAuth() { + return pb.authStore.isValid; +} + +// Handle login form submission +document.addEventListener('DOMContentLoaded', () => { + const loginForm = document.getElementById('loginForm'); + const errorMessage = document.getElementById('errorMessage'); + + if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const submitButton = document.getElementById('submitButton'); + const originalButtonText = submitButton.textContent; + + // Disable button and show loading state + submitButton.disabled = true; + submitButton.textContent = 'Logging in...'; + errorMessage.textContent = ''; + errorMessage.classList.add('hidden'); + + try { + // Authenticate with PocketBase (automatically stores in cookies) + const authData = await pb.collection('users').authWithPassword(email, password); + + // Store token in localStorage as backup + if (authData && authData.token) { + localStorage.setItem('pb_auth_token', authData.token); + localStorage.setItem('pb_auth_model', JSON.stringify(authData.record)); + } + + // Redirect to dashboard + window.location.href = '/dashboard.html'; + } catch (error) { + console.error('Login error:', error); + errorMessage.textContent = error.message || 'Invalid email or password'; + errorMessage.classList.remove('hidden'); + submitButton.disabled = false; + submitButton.textContent = originalButtonText; + } + }); + } + + // If already authenticated, redirect to dashboard + if (checkAuth() && (window.location.pathname === '/index.html' || window.location.pathname === '/')) { + window.location.href = '/dashboard.html'; + } +}); + diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..a87001c --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,179 @@ +const POCKETBASE_URL = 'https://pocketbase.ccllc.pro'; + +// Initialize PocketBase client +const pb = new PocketBase(POCKETBASE_URL); + +// Check authentication and load user data +async function initDashboard() { + // Load auth from cookies first + try { + pb.authStore.loadFromCookie(document.cookie); + } catch (e) { + console.error('Error loading from cookie:', e); + } + + // If not valid from cookie, try loading from localStorage + if (!pb.authStore.isValid) { + 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 (e) { + console.error('Error loading from localStorage:', e); + } + } + } + + // Check if user is authenticated + if (!pb.authStore.isValid) { + // Try to refresh the token if we have a token but it's expired + if (pb.authStore.token) { + try { + 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, clear everything and redirect to login + pb.authStore.clear(); + localStorage.removeItem('pb_auth_token'); + localStorage.removeItem('pb_auth_model'); + window.location.href = '/index.html'; + return; + } + } else { + // No token at all, redirect to login + localStorage.removeItem('pb_auth_token'); + localStorage.removeItem('pb_auth_model'); + window.location.href = '/index.html'; + return; + } + } + + try { + // Display user information + const user = pb.authStore.model; + if (user) { + const userEmailElement = document.getElementById('userEmail'); + const userNameElement = document.getElementById('userName'); + + if (userEmailElement) { + userEmailElement.textContent = user.email || 'User'; + } + if (userNameElement) { + userNameElement.textContent = user.username || user.email || 'User'; + } + } else { + // No user model, redirect to login + console.error('No user model found'); + pb.authStore.clear(); + localStorage.removeItem('pb_auth_token'); + localStorage.removeItem('pb_auth_model'); + window.location.href = '/index.html'; + } + } catch (error) { + console.error('Dashboard init error:', error); + pb.authStore.clear(); + localStorage.removeItem('pb_auth_token'); + localStorage.removeItem('pb_auth_model'); + window.location.href = '/index.html'; + } +} + +// Get auth token for API requests +function getAuthToken() { + return pb.authStore.token || localStorage.getItem('pb_auth_token') || ''; +} + +// Load dashboard statistics +async function loadStats() { + try { + const token = getAuthToken(); + if (!token) { + return; + } + + // Load employees stats + try { + const employeesResponse = await fetch('/api/employees?page=1&perPage=1000', { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (employeesResponse.ok) { + const employeesData = await employeesResponse.json(); + const totalEmployeesElement = document.getElementById('totalEmployees'); + if (totalEmployeesElement) { + totalEmployeesElement.innerHTML = employeesData.totalItems || 0; + } + + // Count active employees (those with Type = "Employee") + const activeCount = employeesData.items?.filter(emp => emp.Type === 'Employee').length || 0; + const activeEmployeesElement = document.getElementById('activeEmployees'); + if (activeEmployeesElement) { + activeEmployeesElement.innerHTML = activeCount; + } + } + } catch (error) { + console.error('Error loading employee stats:', error); + } + + // Load reports stats + try { + const reportsResponse = await fetch('/api/reports?page=1&perPage=1', { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (reportsResponse.ok) { + const reportsData = await reportsResponse.json(); + const totalReportsElement = document.getElementById('totalReports'); + if (totalReportsElement) { + totalReportsElement.innerHTML = reportsData.totalItems || 0; + } + } + } catch (error) { + console.error('Error loading report stats:', error); + } + } catch (error) { + console.error('Error loading stats:', error); + } +} + +// Handle logout +function handleLogout() { + 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); + // Clear auth store anyway + pb.authStore.clear(); + localStorage.removeItem('pb_auth_token'); + localStorage.removeItem('pb_auth_model'); + window.location.href = '/index.html'; + } + }); + } +} + +// Initialize dashboard when page loads +document.addEventListener('DOMContentLoaded', async () => { + await initDashboard(); + handleLogout(); + loadStats(); +}); + diff --git a/public/js/employees.js b/public/js/employees.js new file mode 100644 index 0000000..8648d58 --- /dev/null +++ b/public/js/employees.js @@ -0,0 +1,985 @@ +const POCKETBASE_URL = 'https://pocketbase.ccllc.pro'; +const pb = new PocketBase(POCKETBASE_URL); + +// Load auth from localStorage or 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); + } + } +} + +let currentPage = 1; +const perPage = 50; +let allEmployees = []; +let currentFilter = 'all'; +let currentSearch = ''; +let currentEditingEmployeeId = null; +let quickReportEditor = null; + +// 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 ''; +} + +// 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 = ` +
+ ${message} +
+ `; + + setTimeout(() => { + container.innerHTML = ''; + }, 5000); +} + +// Load employees +async function loadEmployees(page = 1) { + try { + const token = getAuthToken(); + console.log('Loading employees, token present:', !!token); + + if (!token) { + console.error('No auth token found'); + window.location.href = '/index.html'; + return; + } + + const url = `/api/employees?page=${page}&perPage=${perPage}&sort=Last_Name`; + console.log('Fetching from:', url); + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + console.log('Response status:', response.status); + + if (response.status === 401) { + console.error('Unauthorized - redirecting to login'); + window.location.href = '/index.html'; + return; + } + + if (!response.ok) { + const error = await response.json(); + console.error('Error response:', error); + throw new Error(error.error || 'Failed to load employees'); + } + + const data = await response.json(); + console.log('Successfully loaded employees:', data); + allEmployees = data.items; + applyFiltersAndSearch(); + displayPagination(data); + currentPage = page; + } catch (error) { + console.error('Error loading employees:', error); + showMessage(error.message || 'Failed to load employees', 'error'); + document.getElementById('employeesTableBody').innerHTML = ` + + Error loading employees: ${error.message} + + `; + } +} + +// Filter and search employees +function applyFiltersAndSearch() { + let filtered = [...allEmployees]; + + // Apply type filter + if (currentFilter === 'employee') { + filtered = filtered.filter(emp => emp.Type === 'Employee'); + } else if (currentFilter === 'sub-contractor') { + filtered = filtered.filter(emp => emp.Type === 'Sub-Contractor'); + } + + // Apply search + if (currentSearch.trim()) { + const searchLower = currentSearch.toLowerCase().trim(); + filtered = filtered.filter(emp => { + const firstName = (emp.First_Name || '').toLowerCase(); + const lastName = (emp.Last_Name || '').toLowerCase(); + const id = (emp.id || '').toLowerCase(); + const fullName = `${firstName} ${lastName}`; + return firstName.includes(searchLower) || + lastName.includes(searchLower) || + fullName.includes(searchLower) || + id.includes(searchLower); + }); + } + + // Sort by last name A to Z + filtered.sort((a, b) => { + const lastNameA = (a.Last_Name || '').toLowerCase(); + const lastNameB = (b.Last_Name || '').toLowerCase(); + if (lastNameA < lastNameB) return -1; + if (lastNameA > lastNameB) return 1; + // If last names are the same, sort by first name + const firstNameA = (a.First_Name || '').toLowerCase(); + const firstNameB = (b.First_Name || '').toLowerCase(); + if (firstNameA < firstNameB) return -1; + if (firstNameA > firstNameB) return 1; + return 0; + }); + + displayEmployees(filtered); +} + +// Display employees in table +function displayEmployees(employees) { + const tbody = document.getElementById('employeesTableBody'); + + if (employees.length === 0) { + tbody.innerHTML = ` + + No employees found + + `; + return; + } + + tbody.innerHTML = employees.map(emp => ` + + ${emp.id || '-'} + ${emp.First_Name || '-'} + ${emp.Last_Name || '-'} + ${emp.Type || '-'} + + + + + + `).join(''); +} + +// Update filter button styles +function updateFilterButtons() { + const allBtn = document.getElementById('filterAll'); + const employeeBtn = document.getElementById('filterEmployee'); + const subContractorBtn = document.getElementById('filterSubContractor'); + + // Reset all buttons + [allBtn, employeeBtn, subContractorBtn].forEach(btn => { + btn.classList.remove('bg-indigo-600', 'text-white', 'hover:bg-indigo-700'); + btn.classList.add('bg-gray-200', 'text-gray-700', 'hover:bg-gray-300'); + }); + + // Set active button + let activeBtn; + if (currentFilter === 'all') { + activeBtn = allBtn; + } else if (currentFilter === 'employee') { + activeBtn = employeeBtn; + } else if (currentFilter === 'sub-contractor') { + activeBtn = subContractorBtn; + } + + if (activeBtn) { + activeBtn.classList.remove('bg-gray-200', 'text-gray-700', 'hover:bg-gray-300'); + activeBtn.classList.add('bg-indigo-600', 'text-white', 'hover:bg-indigo-700'); + } +} + +// Display pagination +function displayPagination(data) { + const pagination = document.getElementById('pagination'); + + if (data.totalPages <= 1) { + pagination.innerHTML = `
Total: ${data.totalItems} employees
`; + return; + } + + pagination.innerHTML = ` +
+ Showing ${(data.page - 1) * data.perPage + 1} to ${Math.min(data.page * data.perPage, data.totalItems)} of ${data.totalItems} employees +
+
+ + Page ${data.page} of ${data.totalPages} + +
+ `; +} + +// Open modal for adding employee +function openAddModal() { + currentEditingEmployeeId = null; + document.getElementById('modalTitle').textContent = 'Add Employee'; + document.getElementById('employeeForm').reset(); + document.getElementById('employeeId').value = ''; + + // Hide Reports tab for new employees + document.getElementById('employeeReportsTab').style.display = 'none'; + + // Show Employee Info tab + switchToEmployeeInfoTab(); + + document.getElementById('employeeModal').classList.remove('hidden'); +} + +// Switch to Employee Info tab +function switchToEmployeeInfoTab() { + document.getElementById('employeeInfoTab').classList.add('border-indigo-500', 'text-indigo-600'); + document.getElementById('employeeInfoTab').classList.remove('border-transparent', 'text-gray-500'); + document.getElementById('employeeReportsTab').classList.remove('border-indigo-500', 'text-indigo-600'); + document.getElementById('employeeReportsTab').classList.add('border-transparent', 'text-gray-500'); + document.getElementById('employeeInfoContent').classList.remove('hidden'); + document.getElementById('employeeReportsContent').classList.add('hidden'); +} + +// Switch to Reports tab +function switchToReportsTab() { + document.getElementById('employeeReportsTab').classList.add('border-indigo-500', 'text-indigo-600'); + document.getElementById('employeeReportsTab').classList.remove('border-transparent', 'text-gray-500'); + document.getElementById('employeeInfoTab').classList.remove('border-indigo-500', 'text-indigo-600'); + document.getElementById('employeeInfoTab').classList.add('border-transparent', 'text-gray-500'); + document.getElementById('employeeInfoContent').classList.add('hidden'); + document.getElementById('employeeReportsContent').classList.remove('hidden'); +} + +// Initialize quick report editor +function initQuickReportEditor() { + if (!quickReportEditor) { + const editorContainer = document.getElementById('quickReportEditor'); + if (editorContainer && typeof Quill !== 'undefined') { + try { + quickReportEditor = new Quill('#quickReportEditor', { + theme: 'snow', + modules: { + toolbar: [ + [{ 'header': [1, 2, 3, false] }], + ['bold', 'italic', 'underline'], + [{ 'list': 'ordered'}, { 'list': 'bullet' }], + ['link'], + ['clean'] + ] + } + }); + + // Force text color to be dark + const style = document.createElement('style'); + style.textContent = ` + #quickReportEditor .ql-editor { + color: #1f2937 !important; + background-color: #ffffff !important; + } + #quickReportEditor .ql-editor.ql-blank::before { + color: #9ca3af !important; + } + #quickReportEditor .ql-editor p, + #quickReportEditor .ql-editor h1, + #quickReportEditor .ql-editor h2, + #quickReportEditor .ql-editor h3 { + color: #1f2937 !important; + } + `; + document.head.appendChild(style); + + quickReportEditor.on('text-change', function() { + const htmlContent = quickReportEditor.root.innerHTML; + document.getElementById('quickReport').value = htmlContent; + }); + } catch (error) { + console.error('Error initializing quick report editor:', error); + } + } + } + return quickReportEditor; +} + +// Load employee reports +async function loadEmployeeReports(employeeId) { + try { + const token = getAuthToken(); + const response = await fetch(`/api/reports?employee=${employeeId}&perPage=1000&sort=-Report_Date`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to load reports'); + } + + const data = await response.json(); + displayEmployeeReports(data.items || []); + } catch (error) { + console.error('Error loading employee reports:', error); + document.getElementById('employeeReportsList').innerHTML = '
Failed to load reports
'; + } +} + +// Display employee reports in post board style +function displayEmployeeReports(reports) { + const container = document.getElementById('employeeReportsList'); + + if (reports.length === 0) { + container.innerHTML = '
No reports yet. Add one to get started!
'; + return; + } + + const reportsHtml = reports.map(report => { + // 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' + }) + : 'No date'; + + const sentimentColor = { + 'Positive': 'bg-green-100 text-green-800', + 'Negative': 'bg-red-100 text-red-800', + 'Neutral': 'bg-gray-100 text-gray-800' + }[report.Centiment] || 'bg-gray-100 text-gray-800'; + + const reportText = report.Report || ''; + + // Collect all links and 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) }); + + return ` +
+
+
+ 📅 ${reportDate} + ${report.Centiment ? `${escapeHtml(report.Centiment)}` : ''} +
+
+ + +
+
+
+ ${report.Report || 'No content'} +
+ ${links.length > 0 ? ` +
+
Links:
+
+ ${links.map(link => { + if (link.isVoxer) { + return ` + + 🎙️ Voxer ${link.num} + + `; + } else { + return ` + + 🔗 Link ${link.num} + + `; + } + }).join('')} +
+
+ ` : ''} +
+ `; + }).join(''); + + container.innerHTML = reportsHtml; +} + +// Edit employee report +let currentEditingReportId = null; + +window.editEmployeeReport = async function(reportId) { + try { + currentEditingReportId = reportId; + const token = getAuthToken(); + const response = await fetch(`/api/reports/${reportId}`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to load report'); + } + + const report = await response.json(); + + // Populate the quick report form + initQuickReportEditor(); + + // Set rich text editor content + const reportContent = report.Report || ''; + setTimeout(() => { + if (quickReportEditor) { + quickReportEditor.root.innerHTML = reportContent || '


'; + document.getElementById('quickReport').value = reportContent; + } + }, 100); + + document.getElementById('quickSentiment').value = report.Centiment || ''; + + // Format date and time for input field (convert from UTC to local) + if (report.Report_Date) { + const date = new Date(report.Report_Date); + 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('quickReportDate').value = `${year}-${month}-${day}T${hours}:${minutes}`; + } else { + document.getElementById('quickReportDate').value = ''; + } + + // Set links + document.getElementById('quickLink01').value = report.Link_01 || ''; + document.getElementById('quickLink02').value = report.Link_02 || ''; + document.getElementById('quickLink03').value = report.Link_03 || ''; + document.getElementById('quickLink04').value = report.Link_04 || ''; + document.getElementById('quickLink05').value = report.Link_05 || ''; + + // Update form title + const formTitle = document.querySelector('#addReportForm h4'); + if (formTitle) { + formTitle.textContent = 'Edit Report'; + } + + // Show the form + document.getElementById('addReportForm').classList.remove('hidden'); + + // Scroll to form + document.getElementById('addReportForm').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } catch (error) { + console.error('Error loading report:', error); + showMessage('Failed to load report', 'error'); + } +}; + +// Delete employee report +window.deleteEmployeeReport = async function(reportId) { + if (!confirm('Are you sure you want to delete this report?')) { + return; + } + + try { + const token = getAuthToken(); + const response = await fetch(`/api/reports/${reportId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to delete report'); + } + + // Reload reports + if (currentEditingEmployeeId) { + await loadEmployeeReports(currentEditingEmployeeId); + } + showMessage('Report deleted successfully', 'success'); + } catch (error) { + console.error('Error deleting report:', error); + showMessage('Failed to delete report', 'error'); + } +}; + +// Edit employee +async function editEmployee(id) { + try { + currentEditingEmployeeId = id; + const token = getAuthToken(); + const response = await fetch(`/api/employees/${id}`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to load employee'); + } + + const employee = await response.json(); + + document.getElementById('modalTitle').textContent = 'Edit Employee'; + document.getElementById('employeeId').value = employee.id || ''; + document.getElementById('firstName').value = employee.First_Name || ''; + document.getElementById('lastName').value = employee.Last_Name || ''; + document.getElementById('email').value = employee.Email || ''; + document.getElementById('phoneNumber').value = employee.Phone_Number || ''; + document.getElementById('voxerId').value = employee.Voxer_ID || ''; + document.getElementById('type').value = employee.Type || ''; + + // Show Reports tab button + document.getElementById('employeeReportsTab').style.display = 'block'; + + // Show Employee Info tab by default + switchToEmployeeInfoTab(); + + // Load employee reports + await loadEmployeeReports(id); + + document.getElementById('employeeModal').classList.remove('hidden'); + } catch (error) { + console.error('Error loading employee:', error); + showMessage('Failed to load employee', 'error'); + } +} + +// Delete employee +async function deleteEmployee(id) { + if (!confirm('Are you sure you want to delete this employee?')) { + return; + } + + try { + const token = getAuthToken(); + const response = await fetch(`/api/employees/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete employee'); + } + + showMessage('Employee deleted successfully'); + // Reset search and filter when reloading + currentSearch = ''; + currentFilter = 'all'; + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.value = ''; + } + updateFilterButtons(); + loadEmployees(currentPage); + } catch (error) { + console.error('Error deleting employee:', error); + showMessage(error.message || 'Failed to delete employee', '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', () => { + // Initialize page (auth check, user display, logout) + initPage(); + + // Load employees on page load + loadEmployees(); + + // Add employee button + document.getElementById('addEmployeeBtn').addEventListener('click', openAddModal); + + // Cancel button + document.getElementById('cancelBtn').addEventListener('click', () => { + document.getElementById('employeeModal').classList.add('hidden'); + document.getElementById('addReportForm').classList.add('hidden'); + if (quickReportEditor) { + quickReportEditor.setContents([]); + quickReportEditor.root.innerHTML = '


'; + } + }); + + // Close modal button + const closeModalBtn = document.getElementById('closeEmployeeModal'); + if (closeModalBtn) { + closeModalBtn.addEventListener('click', () => { + document.getElementById('employeeModal').classList.add('hidden'); + document.getElementById('addReportForm').classList.add('hidden'); + if (quickReportEditor) { + quickReportEditor.setContents([]); + quickReportEditor.root.innerHTML = '


'; + } + }); + } + + // Tab switching + document.getElementById('employeeInfoTab').addEventListener('click', switchToEmployeeInfoTab); + document.getElementById('employeeReportsTab').addEventListener('click', switchToReportsTab); + + // Add Report button + const addReportBtn = document.getElementById('addReportBtn'); + if (addReportBtn) { + addReportBtn.addEventListener('click', () => { + currentEditingReportId = null; + initQuickReportEditor(); + document.getElementById('addReportForm').classList.remove('hidden'); + // Set current date/time as default + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + document.getElementById('quickReportDate').value = `${year}-${month}-${day}T${hours}:${minutes}`; + // Reset form title + const formTitle = document.querySelector('#addReportForm h4'); + if (formTitle) { + formTitle.textContent = 'Add New Report'; + } + }); + } + + // Cancel quick report + const cancelQuickReport = document.getElementById('cancelQuickReport'); + if (cancelQuickReport) { + cancelQuickReport.addEventListener('click', () => { + document.getElementById('addReportForm').classList.add('hidden'); + currentEditingReportId = null; + if (quickReportEditor) { + quickReportEditor.setContents([]); + quickReportEditor.root.innerHTML = '


'; + } + document.getElementById('quickReportForm').reset(); + // Clear all link fields + document.getElementById('quickLink01').value = ''; + document.getElementById('quickLink02').value = ''; + document.getElementById('quickLink03').value = ''; + document.getElementById('quickLink04').value = ''; + document.getElementById('quickLink05').value = ''; + // Reset form title + const formTitle = document.querySelector('#addReportForm h4'); + if (formTitle) { + formTitle.textContent = 'Add New Report'; + } + }); + } + + // Quick report form submission + const quickReportForm = document.getElementById('quickReportForm'); + if (quickReportForm) { + quickReportForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + if (!currentEditingEmployeeId) { + showMessage('Please save the employee first', 'error'); + return; + } + + try { + const token = getAuthToken(); + + // Convert datetime-local to UTC ISO string + let reportDateUTC = undefined; + const reportDateValue = document.getElementById('quickReportDate').value; + if (reportDateValue) { + const localDate = new Date(reportDateValue); + reportDateUTC = localDate.toISOString(); + } + + const formData = { + Employee: currentEditingEmployeeId, + Report: document.getElementById('quickReport').value || undefined, + Centiment: document.getElementById('quickSentiment').value || undefined, + Report_Date: reportDateUTC, + Link_01: document.getElementById('quickLink01').value || undefined, + Link_02: document.getElementById('quickLink02').value || undefined, + Link_03: document.getElementById('quickLink03').value || undefined, + Link_04: document.getElementById('quickLink04').value || undefined, + Link_05: document.getElementById('quickLink05').value || undefined, + }; + + const url = currentEditingReportId ? `/api/reports/${currentEditingReportId}` : '/api/reports'; + const method = currentEditingReportId ? '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 ${currentEditingReportId ? 'updated' : 'added'} successfully`, 'success'); + document.getElementById('addReportForm').classList.add('hidden'); + quickReportForm.reset(); + currentEditingReportId = null; + if (quickReportEditor) { + quickReportEditor.setContents([]); + quickReportEditor.root.innerHTML = '


'; + } + // Clear all link fields + document.getElementById('quickLink01').value = ''; + document.getElementById('quickLink02').value = ''; + document.getElementById('quickLink03').value = ''; + document.getElementById('quickLink04').value = ''; + document.getElementById('quickLink05').value = ''; + + // Update form title back to "Add New Report" + const formTitle = document.querySelector('#addReportForm h4'); + if (formTitle) { + formTitle.textContent = 'Add New Report'; + } + + // Reload reports + await loadEmployeeReports(currentEditingEmployeeId); + } catch (error) { + console.error('Error saving report:', error); + showMessage(error.message || 'Failed to save report', 'error'); + } + }); + } + + // Search input + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('input', (e) => { + currentSearch = e.target.value; + applyFiltersAndSearch(); + }); + } + + // Filter buttons + const filterAll = document.getElementById('filterAll'); + const filterEmployee = document.getElementById('filterEmployee'); + const filterSubContractor = document.getElementById('filterSubContractor'); + + if (filterAll) { + filterAll.addEventListener('click', () => { + currentFilter = 'all'; + updateFilterButtons(); + applyFiltersAndSearch(); + }); + } + + if (filterEmployee) { + filterEmployee.addEventListener('click', () => { + currentFilter = 'employee'; + updateFilterButtons(); + applyFiltersAndSearch(); + }); + } + + if (filterSubContractor) { + filterSubContractor.addEventListener('click', () => { + currentFilter = 'sub-contractor'; + updateFilterButtons(); + applyFiltersAndSearch(); + }); + } + + // Initialize filter button styles + updateFilterButtons(); + + // Form submission + document.getElementById('employeeForm').addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(e.target); + const employeeId = formData.get('id'); + const data = { + First_Name: formData.get('First_Name') || '', + Last_Name: formData.get('Last_Name') || '', + Email: formData.get('Email') || '', + Phone_Number: formData.get('Phone_Number') || '', + Voxer_ID: formData.get('Voxer_ID') || '', + Type: formData.get('Type') || '', + }; + + // Note: ID is only needed in URL for updates, not in body + // For creates, ID is optional and auto-generated if not provided + + try { + const token = getAuthToken(); + const url = employeeId ? `/api/employees/${employeeId}` : '/api/employees'; + const method = employeeId ? 'PATCH' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to save employee'); + } + + showMessage(`Employee ${employeeId ? 'updated' : 'created'} successfully`); + document.getElementById('employeeModal').classList.add('hidden'); + // Reset search and filter when reloading + currentSearch = ''; + currentFilter = 'all'; + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.value = ''; + } + updateFilterButtons(); + loadEmployees(currentPage); + } catch (error) { + console.error('Error saving employee:', error); + showMessage(error.message || 'Failed to save employee', 'error'); + } + }); +}); + +// Utility function to escape HTML +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Make functions globally available +window.editEmployee = editEmployee; +window.deleteEmployee = deleteEmployee; +window.loadEmployees = loadEmployees; + diff --git a/public/js/reports.js b/public/js/reports.js new file mode 100644 index 0000000..8253511 --- /dev/null +++ b/public/js/reports.js @@ -0,0 +1,632 @@ +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 = ` +
+ ${message} +
+ `; + + 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 `🎙️ Voxer ${link.num}`; + } else { + return `🔗 Link ${link.num}`; + } + }).join('') + : '-'; + + row.innerHTML = ` + ${escapeHtml(employeeName)} + +
+ ${reportPreview !== '-' ? escapeHtml(reportPreview) : '-'} +
+ + ${escapeHtml(report.Centiment || '-')} + ${escapeHtml(reportDate)} + +
+ ${linksHtml === '-' ? '-' : linksHtml} +
+ + + + + + `; + tableBody.appendChild(row); + }); +} + +// Render pagination +function renderPagination() { + const container = document.getElementById('paginationContainer'); + if (totalPages <= 1) { + container.innerHTML = ''; + return; + } + + let html = '
'; + html += '
'; + if (currentPage > 1) { + html += ``; + } + if (currentPage < totalPages) { + html += ``; + } + html += '
'; + html += '
'; + 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 = '


'; + 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 || '


'; + document.getElementById('Report').value = reportContent; + } else { + // Editor not initialized yet, try again after a short delay + setTimeout(() => { + if (reportEditor) { + reportEditor.root.innerHTML = reportContent || '


'; + 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; +} + diff --git a/public/reports.html b/public/reports.html new file mode 100644 index 0000000..3db96e4 --- /dev/null +++ b/public/reports.html @@ -0,0 +1,181 @@ + + + + + + Employee Reports - Employee Records + + + + + + + + + + + +
+ +
+
+

Employee Reports

+

Manage employee report records

+
+ +
+ + +
+ + +
+
+

Loading reports...

+
+ + + + + + +
+ + + + + + + + diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..da3b3e3 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,47 @@ +import { Context, Next } from 'hono'; +import PocketBase from 'pocketbase'; + +const POCKETBASE_URL = process.env.POCKETBASE_URL || 'https://pocketbase.ccllc.pro'; + +export interface AuthUser { + id: string; + email: string; + [key: string]: any; +} + +export async function authMiddleware(c: Context, next: Next) { + const authHeader = c.req.header('Authorization'); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const token = authHeader.substring(7); + + if (!token || token.trim() === '') { + return c.json({ error: 'No token provided' }, 401); + } + + try { + const pb = new PocketBase(POCKETBASE_URL); + + // Set the token in auth store + // The token will be automatically included in API requests + pb.authStore.save(token, null); + + // Attach PocketBase instance to context for use in routes + c.set('pb', pb); + c.set('token', token); + + // If we have a model from the token, attach it + if (pb.authStore.model) { + c.set('user', pb.authStore.model); + } + + await next(); + } catch (error: any) { + console.error('Auth middleware error:', error); + return c.json({ error: 'Authentication failed: ' + (error.message || 'Unknown error') }, 401); + } +} + diff --git a/src/routes/api.ts b/src/routes/api.ts new file mode 100644 index 0000000..05afa31 --- /dev/null +++ b/src/routes/api.ts @@ -0,0 +1,11 @@ +import { Hono } from 'hono'; + +const api = new Hono(); + +// Placeholder for future API routes +api.get('/health', (c) => { + return c.json({ status: 'ok', message: 'API is running' }); +}); + +export default api; + diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..b7bb733 --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,96 @@ +import { Hono } from 'hono'; +import PocketBase from 'pocketbase'; + +const POCKETBASE_URL = process.env.POCKETBASE_URL || 'https://pocketbase.ccllc.pro'; + +const auth = new Hono(); + +// Login endpoint +auth.post('/login', async (c) => { + try { + const { email, password } = await c.req.json(); + + if (!email || !password) { + return c.json({ error: 'Email and password are required' }, 400); + } + + const pb = new PocketBase(POCKETBASE_URL); + + // Authenticate with PocketBase + const authData = await pb.collection('users').authWithPassword(email, password); + + return c.json({ + token: authData.token, + user: { + id: authData.record.id, + email: authData.record.email, + username: authData.record.username, + created: authData.record.created, + updated: authData.record.updated, + }, + }); + } catch (error: any) { + return c.json( + { error: error.message || 'Login failed' }, + 401 + ); + } +}); + +// Logout endpoint +auth.post('/logout', async (c) => { + try { + const authHeader = c.req.header('Authorization'); + + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); + const pb = new PocketBase(POCKETBASE_URL); + pb.authStore.save(token, null); + await pb.authStore.clear(); + } + + return c.json({ message: 'Logged out successfully' }); + } catch (error: any) { + return c.json({ error: error.message || 'Logout failed' }, 500); + } +}); + +// Get current user session +auth.get('/me', async (c) => { + try { + const authHeader = c.req.header('Authorization'); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const token = authHeader.substring(7); + const pb = new PocketBase(POCKETBASE_URL); + pb.authStore.save(token, null); + + if (!pb.authStore.isValid) { + return c.json({ error: 'Invalid token' }, 401); + } + + const user = pb.authStore.model; + + if (!user) { + return c.json({ error: 'User not found' }, 401); + } + + return c.json({ + user: { + id: user.id, + email: user.email, + username: user.username, + created: user.created, + updated: user.updated, + }, + }); + } catch (error: any) { + return c.json({ error: error.message || 'Authentication failed' }, 401); + } +}); + +export default auth; + diff --git a/src/routes/employees.ts b/src/routes/employees.ts new file mode 100644 index 0000000..3317dbc --- /dev/null +++ b/src/routes/employees.ts @@ -0,0 +1,149 @@ +import { Hono } from 'hono'; +import PocketBase from 'pocketbase'; +import { authMiddleware } from '../middleware/auth'; + +const POCKETBASE_URL = process.env.POCKETBASE_URL || 'https://pocketbase.ccllc.pro'; + +const employees = new Hono(); + +// Initialize PocketBase helper +function getPocketBase(token: string): PocketBase { + const pb = new PocketBase(POCKETBASE_URL); + if (token) { + // Set the auth token - this will be used for API requests + pb.authStore.save(token, null); + } + return pb; +} + +// Get all employee records +employees.get('/', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const page = parseInt(c.req.query('page') || '1'); + const perPage = parseInt(c.req.query('perPage') || '50'); + const sort = c.req.query('sort') || 'Last_Name'; + + console.log('Fetching employee records with token:', token ? 'Token present' : 'No token'); + console.log('PocketBase URL:', POCKETBASE_URL); + + const records = await pb.collection('employee_records').getList(page, perPage, { + sort: sort, + }); + + console.log('Successfully fetched', records.items.length, 'records'); + + return c.json({ + items: records.items, + page: records.page, + perPage: records.perPage, + totalItems: records.totalItems, + totalPages: records.totalPages, + }); + } catch (error: any) { + console.error('Error fetching employee records:', error); + console.error('Error details:', { + message: error.message, + status: error.status, + response: error.response, + }); + return c.json({ + error: error.message || 'Failed to fetch employee records', + details: error.response || error + }, error.status || 500); + } +}); + +// Get a single employee record by ID +employees.get('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + const id = c.req.param('id'); + + const record = await pb.collection('employee_records').getOne(id); + + return c.json(record); + } catch (error: any) { + console.error('Error fetching employee record:', error); + return c.json({ error: error.message || 'Failed to fetch employee record' }, 500); + } +}); + +// Create a new employee record +employees.post('/', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + const body = await c.req.json(); + + // Extract only the fields we expect + const recordData: any = {}; + if (body.id) recordData.id = body.id; + if (body.First_Name) recordData.First_Name = body.First_Name; + if (body.Last_Name) recordData.Last_Name = body.Last_Name; + if (body.Email) recordData.Email = body.Email; + if (body.Phone_Number) recordData.Phone_Number = body.Phone_Number; + if (body.Voxer_ID) recordData.Voxer_ID = body.Voxer_ID; + if (body.Type) recordData.Type = body.Type; + + const record = await pb.collection('employee_records').create(recordData); + + return c.json(record, 201); + } catch (error: any) { + console.error('Error creating employee record:', error); + return c.json({ error: error.message || 'Failed to create employee record' }, 500); + } +}); + +// Update an employee record +employees.patch('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + const id = c.req.param('id'); + const body = await c.req.json(); + + // Extract only the fields we expect + const recordData: any = {}; + if (body.First_Name !== undefined) recordData.First_Name = body.First_Name; + if (body.Last_Name !== undefined) recordData.Last_Name = body.Last_Name; + if (body.Email !== undefined) recordData.Email = body.Email; + if (body.Phone_Number !== undefined) recordData.Phone_Number = body.Phone_Number; + if (body.Voxer_ID !== undefined) recordData.Voxer_ID = body.Voxer_ID; + if (body.Type !== undefined) recordData.Type = body.Type; + + const record = await pb.collection('employee_records').update(id, recordData); + + return c.json(record); + } catch (error: any) { + console.error('Error updating employee record:', error); + return c.json({ error: error.message || 'Failed to update employee record' }, 500); + } +}); + +// Delete an employee record +employees.delete('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + const id = c.req.param('id'); + + await pb.collection('employee_records').delete(id); + + return c.json({ message: 'Employee record deleted successfully' }); + } catch (error: any) { + console.error('Error deleting employee record:', error); + return c.json({ error: error.message || 'Failed to delete employee record' }, 500); + } +}); + +export default employees; + diff --git a/src/routes/reports.ts b/src/routes/reports.ts new file mode 100644 index 0000000..1f95e97 --- /dev/null +++ b/src/routes/reports.ts @@ -0,0 +1,161 @@ +import { Hono } from 'hono'; +import PocketBase from 'pocketbase'; +import { authMiddleware } from '../middleware/auth'; + +const POCKETBASE_URL = process.env.POCKETBASE_URL || 'https://pocketbase.ccllc.pro'; + +const reports = new Hono(); + +// Initialize PocketBase helper +function getPocketBase(token: string): PocketBase { + const pb = new PocketBase(POCKETBASE_URL); + if (token) { + pb.authStore.save(token, null); + } + return pb; +} + +// Get all employee reports +reports.get('/', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const page = parseInt(c.req.query('page') || '1'); + const perPage = parseInt(c.req.query('perPage') || '50'); + const sort = c.req.query('sort') || '-created'; + const employeeId = c.req.query('employee'); // Filter by employee ID + + const filterOptions: any = { + sort: sort, + expand: 'Employee', // Expand the Employee relation to get employee details + }; + + // Add filter if employee ID is provided + if (employeeId) { + filterOptions.filter = `Employee = "${employeeId}"`; + } + + const records = await pb.collection('employee_reports').getList(page, perPage, filterOptions); + + return c.json({ + items: records.items, + page: records.page, + perPage: records.perPage, + totalItems: records.totalItems, + totalPages: records.totalPages, + }); + } catch (error: any) { + console.error('Error fetching reports:', error); + return c.json({ error: error.message || 'Failed to fetch reports' }, 500); + } +}); + +// Get a single employee report by ID +reports.get('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const id = c.req.param('id'); + const record = await pb.collection('employee_reports').getOne(id, { + expand: 'Employee', // Expand the Employee relation to get employee details + }); + + return c.json(record); + } catch (error: any) { + console.error('Error fetching report:', error); + if (error.status === 404) { + return c.json({ error: 'Report not found' }, 404); + } + return c.json({ error: error.message || 'Failed to fetch report' }, 500); + } +}); + +// Create a new employee report +reports.post('/', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const body = await c.req.json(); + + // Build data object with all employee_reports fields + const data: any = {}; + if (body.Report !== undefined) data.Report = body.Report; + if (body.Employee !== undefined) data.Employee = body.Employee; + if (body.Centiment !== undefined) data.Centiment = body.Centiment; + if (body.Report_Date !== undefined) data.Report_Date = body.Report_Date; + if (body.Link_01 !== undefined) data.Link_01 = body.Link_01; + if (body.Link_02 !== undefined) data.Link_02 = body.Link_02; + if (body.Link_03 !== undefined) data.Link_03 = body.Link_03; + if (body.Link_04 !== undefined) data.Link_04 = body.Link_04; + if (body.Link_05 !== undefined) data.Link_05 = body.Link_05; + + const record = await pb.collection('employee_reports').create(data); + + return c.json(record, 201); + } catch (error: any) { + console.error('Error creating report:', error); + return c.json({ error: error.message || 'Failed to create report' }, 500); + } +}); + +// Update an employee report +reports.patch('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const id = c.req.param('id'); + const body = await c.req.json(); + + const data: any = {}; + if (body.Report !== undefined) data.Report = body.Report; + if (body.Employee !== undefined) data.Employee = body.Employee; + if (body.Centiment !== undefined) data.Centiment = body.Centiment; + if (body.Report_Date !== undefined) data.Report_Date = body.Report_Date; + if (body.Link_01 !== undefined) data.Link_01 = body.Link_01; + if (body.Link_02 !== undefined) data.Link_02 = body.Link_02; + if (body.Link_03 !== undefined) data.Link_03 = body.Link_03; + if (body.Link_04 !== undefined) data.Link_04 = body.Link_04; + if (body.Link_05 !== undefined) data.Link_05 = body.Link_05; + + const record = await pb.collection('employee_reports').update(id, data); + + return c.json(record); + } catch (error: any) { + console.error('Error updating report:', error); + if (error.status === 404) { + return c.json({ error: 'Report not found' }, 404); + } + return c.json({ error: error.message || 'Failed to update report' }, 500); + } +}); + +// Delete an employee report +reports.delete('/:id', authMiddleware, async (c) => { + try { + const authHeader = c.req.header('Authorization'); + const token = authHeader?.substring(7) || ''; + const pb = getPocketBase(token); + + const id = c.req.param('id'); + await pb.collection('employee_reports').delete(id); + + return c.json({ message: 'Report deleted successfully' }); + } catch (error: any) { + console.error('Error deleting report:', error); + if (error.status === 404) { + return c.json({ error: 'Report not found' }, 404); + } + return c.json({ error: error.message || 'Failed to delete report' }, 500); + } +}); + +export default reports; + diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..5866477 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,31 @@ +import { Hono } from 'hono'; +import { serveStatic } from 'hono/bun'; +import authRoutes from './routes/auth'; +import apiRoutes from './routes/api'; +import employeeRoutes from './routes/employees'; +import reportsRoutes from './routes/reports'; + +const app = new Hono(); + +// API routes +app.route('/api/auth', authRoutes); +app.route('/api/employees', employeeRoutes); +app.route('/api/reports', reportsRoutes); +app.route('/api', apiRoutes); + +// Serve static files from public directory +app.use('/*', serveStatic({ root: './public' })); + +// Fallback to index.html for SPA routing +app.get('*', serveStatic({ path: './public/index.html' })); + +const port = parseInt(process.env.PORT || '3000'); + +console.log(`🚀 Server running on http://localhost:${port}`); + +// Use Bun.serve for proper Bun integration +Bun.serve({ + port, + fetch: app.fetch, +}); + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..759ad09 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["bun-types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +