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
+
+
+
+
+
+
+
+
Quick Stats
+
+
+
+ ...
+
+
Total Employees
+
+
+
+ ...
+
+
Reports Generated
+
+
+
+ ...
+
+
Active Employees
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ First Name |
+ Last Name |
+ Type |
+ Actions |
+
+
+
+
+ | Loading... |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Add Employee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 ? `
+
+ ` : ''}
+
+ `;
+ }).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 += '
';
+ html += `
Page ${currentPage} of ${totalPages}
`;
+ html += '
';
+ if (currentPage > 1) {
+ html += ``;
+ }
+ if (currentPage < totalPages) {
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Employee |
+ Report |
+ Sentiment |
+ Report Date |
+ Links |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No reports
+
Get started by creating a new employee report.
+
+
+
+
+
+
+
+
+
+
+
+
Add Employee Report
+
+
+
+
+
+
+
+
+
+
+
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"]
+}
+