diff --git a/README.md b/README.md
index 91fe2ab..bc555ab 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,234 @@
-# Job-Pages
+# Job Tracking System
+A modern, responsive web application for tracking construction/contracting jobs, built with vanilla HTML/CSS/JavaScript and PocketBase.
+
+## Features
+
+- **Job Management**: Create, read, update, and delete job records
+- **Comprehensive Job Information**: Track all job details including:
+ - Basic information (Job Number, Name, Address, Type, Status, Division)
+ - Team assignments (Estimator, Office Rep, Project Manager)
+ - Client information (Company, Contact Person, Phone, Email)
+ - Dates and scheduling (Start Date, Due Date, Submission Date)
+ - Links and references (QB Link, Voxer Link, Folder Link)
+ - Status flags (Active, Tax Exempt, Docs Uploaded, etc.)
+- **Notes System**: Add rich text notes to jobs with sentiment tracking
+- **Search & Filter**: Search jobs by number, name, or client; filter by status
+- **Dashboard**: View job statistics and quick actions
+- **Responsive Design**: Works on mobile, tablet, and desktop
+
+## Technology Stack
+
+- **Frontend**: Vanilla HTML/CSS/JavaScript
+- **CSS Framework**: Tailwind CSS (via CDN)
+- **Rich Text Editor**: Quill.js
+- **Backend**: PocketBase
+- **Design**: Modern, clean UI with Indigo/Green/Purple color scheme
+
+## Setup Instructions
+
+### 1. Install PocketBase
+
+Download PocketBase from [https://pocketbase.io/docs/](https://pocketbase.io/docs/) and start it:
+
+```bash
+./pocketbase serve
+```
+
+By default, PocketBase runs on `http://127.0.0.1:8090`
+
+### 2. Configure PocketBase
+
+1. Open PocketBase Admin UI at `http://127.0.0.1:8090/_/`
+2. Create an admin account
+3. Create the following collections:
+
+#### Job_Info Collection
+
+Create a collection named `Job_Info` with the following fields:
+
+- `id` (Auto) - Auto-generated ID
+- `Job_Number` (Text)
+- `Job_Full_Name` (Text)
+- `Job_Name` (Text)
+- `Job_Address` (Text)
+- `Job_Type` (Text)
+- `Job_Status` (Text)
+- `Job_Division` (Text)
+- `Estimator` (Text)
+- `Office_Rep` (Text)
+- `Project_Manager` (Text)
+- `Company_Client` (Text)
+- `Contact_Person` (Text)
+- `Phone_Number` (Text)
+- `Email` (Text)
+- `Due_Date_Source` (Text)
+- `Due_Date_Counter` (Text)
+- `Due_Time` (Text)
+- `Schedule_Confidence` (Text)
+- `Notes` (Text)
+- `Job_QB_Link` (Text)
+- `Voxer_Link` (Text)
+- `Job_Codes` (Text)
+- `Job_Folder_Link` (Text)
+- `Has_Attachments` (Boolean)
+- `Attachment_List` (JSON)
+- `Note_Ref` (JSON - Array of relation IDs)
+- `latest_note_summary` (JSON)
+- `note_thread_text` (JSON)
+- `note_count` (Number)
+- `last_note_at` (Date)
+- `Job_Number_Parent` (Text)
+- `Start_Date` (Date)
+- `Submission_Date` (Date)
+- `Flag` (Boolean)
+- `Tax_Exempt` (Boolean)
+- `Active` (Boolean)
+- `Due_Date` (Date)
+- `Docs_Uploaded` (Boolean)
+- `Tax_Exempt_Docs_Uploaded` (Boolean)
+- `Estimate_Approved` (Boolean)
+- `QB_Created` (Boolean)
+- `Added_To_Calendar` (Boolean)
+- `PSwift_Uploaded` (Boolean)
+- `Voxer_Created` (Boolean)
+- `Estimate_Reviewed` (Boolean)
+- `Estimate_Draft` (Boolean)
+
+#### Notes Collection
+
+Create a collection named `notes` with the following fields:
+
+- `id` (Auto) - Auto-generated ID
+- `content` (Text) - Rich text content
+- `sentiment` (Text) - Positive, Negative, or Neutral
+- `date` (Date)
+- `job` (Relation) - Link to Job_Info collection (optional)
+
+### 3. Configure PocketBase URL
+
+Update the PocketBase URL in `js/pocketbase.js` if your instance is running on a different address:
+
+```javascript
+const POCKETBASE_URL = 'http://127.0.0.1:8090';
+```
+
+### 4. Set Up Authentication
+
+1. In PocketBase Admin UI, go to Settings > Users
+2. Create user accounts for your team members
+3. Users can log in using their email and password
+
+### 5. Set Collection Permissions
+
+For both `Job_Info` and `notes` collections:
+
+1. Go to the collection settings
+2. Set **List/Search rule**: `@request.auth.id != ""` (authenticated users only)
+3. Set **View rule**: `@request.auth.id != ""` (authenticated users only)
+4. Set **Create rule**: `@request.auth.id != ""` (authenticated users only)
+5. Set **Update rule**: `@request.auth.id != ""` (authenticated users only)
+6. Set **Delete rule**: `@request.auth.id != ""` (authenticated users only)
+
+### 6. Run the Application
+
+1. Install dependencies (if using Bun):
+
+```bash
+bun install
+```
+
+2. Start the development server:
+
+```bash
+bun run dev
+```
+
+The server will start on `http://localhost:3075`
+
+3. Open `http://localhost:3075` in your browser
+4. Log in with your PocketBase user credentials
+
+**Alternative:** If you prefer a different server:
+
+```bash
+# Using Python 3
+python3 -m http.server 8000
+
+# Using Node.js (http-server)
+npx http-server -p 8000
+
+# Using PHP
+php -S localhost:8000
+```
+
+## File Structure
+
+```
+Job-Pages/
+├── index.html # Login page
+├── dashboard.html # Dashboard with stats
+├── jobs.html # Jobs list and management
+├── notes.html # Notes/reports page
+├── js/
+│ ├── pocketbase.js # PocketBase client
+│ ├── auth.js # Authentication
+│ ├── jobs.js # Job CRUD operations
+│ └── notes.js # Notes operations
+├── css/
+│ └── custom.css # Custom styles
+└── README.md # This file
+```
+
+## Usage
+
+### Adding a Job
+
+1. Click "Add Job" on the jobs page
+2. Fill in the job information across the 8 tabs:
+ - Basic Info: Job number, name, address, type, status, division
+ - Team: Estimator, Office Rep, Project Manager
+ - Client: Company, contact person, phone, email
+ - Dates: Start date, due date, submission date, scheduling info
+ - Links: QB link, Voxer link, folder link, job codes
+ - Flags: Checkboxes for various status flags
+ - Notes: Add notes to the job (for existing jobs)
+ - Attachments: View attachments (for existing jobs)
+3. Click "Save Job"
+
+### Adding Notes
+
+1. Open a job for editing
+2. Go to the "Notes" tab
+3. Click "+ Add Note"
+4. Enter note content using the rich text editor
+5. Select sentiment (Positive, Negative, Neutral)
+6. Click "Save Note"
+
+### Searching and Filtering
+
+- Use the search bar to search by job number, name, or client
+- Use filter buttons to show All, Active, or Inactive jobs
+
+## Troubleshooting
+
+### CORS Errors
+
+Make sure you're serving the files through a web server, not opening them directly as files.
+
+### Authentication Issues
+
+- Verify PocketBase is running
+- Check the PocketBase URL in `js/pocketbase.js`
+- Ensure user accounts exist in PocketBase
+- Check collection permissions
+
+### Data Not Loading
+
+- Check browser console for errors
+- Verify PocketBase collections are created correctly
+- Ensure field names match exactly (case-sensitive)
+
+## License
+
+See LICENSE file for details.
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..a222688
--- /dev/null
+++ b/config.json
@@ -0,0 +1,16 @@
+{
+ "pocketbase": {
+ "collections": {
+ "jobs": "Job_Info",
+ "notes": "notes",
+ "users": "users"
+ }
+ },
+ "pagination": {
+ "itemsPerPage": 20
+ },
+ "filters": {
+ "defaultFilter": "all"
+ }
+}
+
diff --git a/css/custom.css b/css/custom.css
new file mode 100644
index 0000000..cfb7887
--- /dev/null
+++ b/css/custom.css
@@ -0,0 +1,37 @@
+/* Custom styles for Job Tracking System */
+
+/* Quill editor custom styles */
+.ql-editor {
+ min-height: 150px;
+}
+
+.ql-container {
+ font-size: 14px;
+}
+
+/* Smooth transitions */
+* {
+ transition-property: background-color, border-color, color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
diff --git a/dashboard.html b/dashboard.html
new file mode 100644
index 0000000..ad388e2
--- /dev/null
+++ b/dashboard.html
@@ -0,0 +1,270 @@
+
+
+
+
+
+ Job Tracking - Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
Welcome
+
Manage your job records and tracking
+
+
+
+
+
+
+
+
Job Tracking
+
View and manage job information
+
View Jobs →
+
+
+
+
+
+
Notes & Reports
+
Manage job notes and reports
+
View Notes →
+
+
+
+
+
+
Quick Stats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+
In Progress
+
+
+
+
+
+ ...
+
+
Billed (IP)
+
+
+
+
+
+ ...
+
+
Billed (Closed)
+
+
+
+
+
+ ...
+
+
Not Awarded
+
+
+
+
+
+ ...
+
+
Not Bidding
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d94db2f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ Job Tracking - Login
+
+
+
+
+
+
+
Job Tracking
+
Sign in to your account
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs.html b/jobs.html
new file mode 100644
index 0000000..01644a7
--- /dev/null
+++ b/jobs.html
@@ -0,0 +1,909 @@
+
+
+
+
+
+ Job Tracking - Jobs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Job Number |
+ Job Name |
+ Client |
+ Estimator |
+ Status |
+ Type |
+ Division |
+ Actions |
+
+
+
+
+ | Loading... |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Job Dashboard
+
+
+ Job Number:
+ -
+
+
+ Status:
+ -
+
+
+
+
+
+
+
Quick Info
+
+
+ Job Type:
+ -
+
+
+ Division:
+ -
+
+
+ Estimator:
+ -
+
+
+ Office Rep:
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/auth.js b/js/auth.js
new file mode 100644
index 0000000..7cc5de3
--- /dev/null
+++ b/js/auth.js
@@ -0,0 +1,41 @@
+// Authentication functions
+
+/**
+ * Login with email and password
+ */
+async function login(email, password) {
+ try {
+ const authData = await pb.collection('users').authWithPassword(email, password);
+ return authData;
+ } catch (error) {
+ console.error('Login error:', error);
+ throw new Error(error.message || 'Login failed. Please check your credentials.');
+ }
+}
+
+/**
+ * Logout current user
+ */
+function logout() {
+ pb.authStore.clear();
+ window.location.href = 'index.html';
+}
+
+/**
+ * Check if user is authenticated, redirect to login if not
+ */
+function checkAuth() {
+ if (!pb.authStore.isValid) {
+ window.location.href = 'index.html';
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Get current user
+ */
+function getCurrentUser() {
+ return pb.authStore.model;
+}
+
diff --git a/js/config.js b/js/config.js
new file mode 100644
index 0000000..73722bf
--- /dev/null
+++ b/js/config.js
@@ -0,0 +1,63 @@
+// Configuration loader
+let appConfig = null;
+
+/**
+ * Load configuration from config.json
+ */
+async function loadConfig() {
+ if (appConfig) {
+ return appConfig;
+ }
+
+ try {
+ const response = await fetch('/config.json');
+ if (!response.ok) {
+ throw new Error(`Failed to load config: ${response.statusText}`);
+ }
+ appConfig = await response.json();
+ return appConfig;
+ } catch (error) {
+ console.error('Error loading config:', error);
+ // Return default config if file can't be loaded
+ return {
+ pocketbase: {
+ collections: {
+ jobs: "Job_Info",
+ notes: "notes",
+ users: "users"
+ }
+ },
+ pagination: {
+ itemsPerPage: 20
+ },
+ filters: {
+ defaultFilter: "all"
+ }
+ };
+ }
+}
+
+/**
+ * Get the jobs collection name
+ */
+async function getJobsCollection() {
+ const config = await loadConfig();
+ return config.pocketbase.collections.jobs;
+}
+
+/**
+ * Get the notes collection name
+ */
+async function getNotesCollection() {
+ const config = await loadConfig();
+ return config.pocketbase.collections.notes;
+}
+
+/**
+ * Get items per page setting
+ */
+async function getItemsPerPage() {
+ const config = await loadConfig();
+ return config.pagination.itemsPerPage || 20;
+}
+
diff --git a/js/jobs.js b/js/jobs.js
new file mode 100644
index 0000000..ca909a8
--- /dev/null
+++ b/js/jobs.js
@@ -0,0 +1,868 @@
+// Job CRUD operations
+
+let currentPage = 1;
+let itemsPerPage = 20;
+let allJobs = [];
+let jobsCollectionName = 'Job_Info'; // Default, will be loaded from config
+let configInitialized = false;
+let configInitializing = false;
+let currentView = localStorage.getItem('jobsView') || 'table'; // 'table' or 'card'
+
+/**
+ * Load all jobs with optional filters
+ */
+async function loadJobs(page = 1) {
+ try {
+ // Show loading state
+ // Show loading state based on current view
+ if (currentView === 'table') {
+ const tbody = document.getElementById('jobsTableBody');
+ if (tbody) {
+ tbody.innerHTML = '| Loading... |
';
+ }
+ } else {
+ const container = document.getElementById('jobsCardContainer');
+ if (container) {
+ container.innerHTML = 'Loading...
';
+ }
+ }
+
+ currentPage = page;
+ const searchTerm = document.getElementById('searchInput')?.value || '';
+ const estimatorFilter = window.currentEstimatorFilter || 'all';
+ const statusFilter = window.currentStatusFilter || 'all';
+
+ // Build estimator filter string
+ let estimatorFilterString = '';
+ if (estimatorFilter && estimatorFilter !== 'all') {
+ // Escape special characters for PocketBase filter
+ const escapedFilter = estimatorFilter.replace(/"/g, '\\"');
+ estimatorFilterString = `Estimator = "${escapedFilter}"`;
+ }
+
+ // Build status filter string
+ let statusFilterString = '';
+ if (statusFilter && statusFilter !== 'all') {
+ // Escape special characters for PocketBase filter
+ const escapedStatus = statusFilter.replace(/"/g, '\\"');
+ statusFilterString = `Job_Status = "${escapedStatus}"`;
+ }
+
+ // Build search filter
+ let searchFilter = '';
+ if (searchTerm) {
+ // Escape special characters for PocketBase filter
+ const escapedTerm = searchTerm.replace(/"/g, '\\"');
+ searchFilter = `Job_Number ~ "${escapedTerm}" || Job_Name ~ "${escapedTerm}" || Company_Client ~ "${escapedTerm}"`;
+ }
+
+ // Combine filters
+ const filterParts = [];
+ if (estimatorFilterString) filterParts.push(`(${estimatorFilterString})`);
+ if (statusFilterString) filterParts.push(`(${statusFilterString})`);
+ if (searchFilter) filterParts.push(`(${searchFilter})`);
+
+ let finalFilter = '';
+ if (filterParts.length > 0) {
+ finalFilter = filterParts.join(' && ');
+ }
+
+ const result = await pb.collection(jobsCollectionName).getList(page, itemsPerPage, {
+ filter: finalFilter || undefined,
+ sort: '-Job_Number',
+ });
+
+ allJobs = result.items;
+ displayJobs(allJobs);
+ displayPagination(result.page, result.totalPages, result.totalItems);
+
+ // Update view based on current view preference
+ updateViewDisplay();
+ } catch (error) {
+ console.error('Error loading jobs:', error);
+ showMessage('Error loading jobs: ' + (error.message || 'Unknown error'), 'error');
+ if (currentView === 'table') {
+ const tbody = document.getElementById('jobsTableBody');
+ if (tbody) {
+ tbody.innerHTML = '| Error loading jobs. Please refresh the page. |
';
+ }
+ } else {
+ const container = document.getElementById('jobsCardContainer');
+ if (container) {
+ container.innerHTML = 'Error loading jobs. Please refresh the page.
';
+ }
+ }
+ }
+}
+
+/**
+ * Get estimator badge HTML
+ */
+function getEstimatorBadgeHtml(estimator) {
+ if (!estimator) return '-';
+
+ const estimatorLower = estimator.toLowerCase();
+ let bgColor = 'bg-gray-100';
+ let textColor = 'text-gray-800';
+
+ if (estimatorLower.includes('steve')) {
+ bgColor = 'bg-orange-100';
+ textColor = 'text-orange-800';
+ } else if (estimatorLower.includes('beth')) {
+ bgColor = 'bg-blue-100';
+ textColor = 'text-blue-800';
+ } else if (estimatorLower.includes('timothy')) {
+ bgColor = 'bg-purple-100';
+ textColor = 'text-purple-800';
+ }
+
+ return `${estimator}`;
+}
+
+/**
+ * Get status badge color
+ */
+function getStatusBadgeColor(status) {
+ if (!status) return 'bg-gray-200 text-gray-700';
+
+ const statusLower = status.toLowerCase();
+
+ if (statusLower.includes('billed') && (statusLower.includes('closed') || statusLower.includes('progress'))) {
+ return 'bg-purple-200 text-purple-800';
+ } else if (statusLower.includes('est hold')) {
+ return 'bg-orange-200 text-orange-800';
+ } else if (statusLower.includes('est sent') || statusLower.includes('follow up')) {
+ return 'bg-yellow-200 text-yellow-800';
+ } else if (statusLower.includes('estimating')) {
+ return 'bg-blue-200 text-blue-800';
+ } else if (statusLower.includes('in progress') || statusLower.includes('awarded')) {
+ return 'bg-green-200 text-green-800';
+ } else if (statusLower.includes('not awarded') || statusLower.includes('not bidding')) {
+ return 'bg-red-200 text-red-800';
+ } else if (statusLower.includes('archive')) {
+ return 'bg-gray-200 text-gray-700';
+ }
+
+ return 'bg-gray-200 text-gray-700';
+}
+
+/**
+ * Display jobs in table or card view
+ */
+function displayJobs(jobs) {
+ if (currentView === 'table') {
+ displayJobsTable(jobs);
+ } else {
+ displayJobsCards(jobs);
+ }
+}
+
+/**
+ * Display jobs in table view
+ */
+function displayJobsTable(jobs) {
+ const tbody = document.getElementById('jobsTableBody');
+ if (!tbody) return;
+
+ if (!jobs || jobs.length === 0) {
+ tbody.innerHTML = '| No jobs found |
';
+ return;
+ }
+
+ tbody.innerHTML = jobs.map(job => {
+ const estimatorDisplay = getEstimatorBadgeHtml(job.Estimator);
+
+ return `
+
+ | ${job.Job_Number || '-'} |
+ ${job.Job_Name || '-'} |
+ ${job.Company_Client || '-'} |
+ ${estimatorDisplay} |
+ ${job.Job_Status || '-'} |
+ ${job.Job_Type || '-'} |
+ ${job.Job_Division || '-'} |
+
+
+ |
+
+ `;
+ }).join('');
+}
+
+/**
+ * Display jobs in card view (multiple lines, single column)
+ */
+function displayJobsCards(jobs) {
+ const container = document.getElementById('jobsCardContainer');
+ if (!container) return;
+
+ if (!jobs || jobs.length === 0) {
+ container.innerHTML = 'No jobs found
';
+ return;
+ }
+
+ container.innerHTML = jobs.map(job => {
+ const estimatorBadge = getEstimatorBadgeHtml(job.Estimator);
+ const statusColor = getStatusBadgeColor(job.Job_Status);
+
+ return `
+
+
+
+
+
${job.Job_Number || 'N/A'}
+
${job.Job_Name || '-'}
+
+
+
+
+
+
+
+
Client
+
${job.Company_Client || '-'}
+
+
+
+
Estimator
+
${estimatorBadge}
+
+
+
+
Status
+
+
+ ${job.Job_Status || '-'}
+
+
+
+
+
+
+
Type
+
${job.Job_Type || '-'}
+
+
+
Division
+
${job.Job_Division || '-'}
+
+
+
+
+ `;
+ }).join('');
+}
+
+/**
+ * Switch between table and card view
+ */
+function switchView(view) {
+ currentView = view;
+ localStorage.setItem('jobsView', view);
+ updateViewDisplay();
+ displayJobs(allJobs);
+}
+
+/**
+ * Update view display based on current view preference
+ */
+function updateViewDisplay() {
+ const tableView = document.getElementById('tableView');
+ const cardView = document.getElementById('cardView');
+ const tableViewBtn = document.getElementById('tableViewBtn');
+ const cardViewBtn = document.getElementById('cardViewBtn');
+
+ if (currentView === 'table') {
+ if (tableView) tableView.classList.remove('hidden');
+ if (cardView) cardView.classList.add('hidden');
+ if (tableViewBtn) {
+ tableViewBtn.classList.add('bg-indigo-600', 'text-white');
+ tableViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
+ }
+ if (cardViewBtn) {
+ cardViewBtn.classList.remove('bg-indigo-600', 'text-white');
+ cardViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
+ }
+ } else {
+ if (tableView) tableView.classList.add('hidden');
+ if (cardView) cardView.classList.remove('hidden');
+ if (tableViewBtn) {
+ tableViewBtn.classList.remove('bg-indigo-600', 'text-white');
+ tableViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
+ }
+ if (cardViewBtn) {
+ cardViewBtn.classList.add('bg-indigo-600', 'text-white');
+ cardViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
+ }
+ }
+}
+
+// Make functions globally accessible
+window.switchView = switchView;
+window.updateViewDisplay = updateViewDisplay;
+
+/**
+ * Display pagination controls
+ */
+function displayPagination(currentPage, totalPages, totalItems) {
+ const container = document.getElementById('paginationContainer');
+ if (!container) return;
+
+ if (totalPages <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+
+ // Previous button
+ if (currentPage > 1) {
+ html += ``;
+ }
+
+ // Page info
+ html += `Page ${currentPage} of ${totalPages} (${totalItems} total)`;
+
+ // Next button
+ if (currentPage < totalPages) {
+ html += ``;
+ }
+
+ html += '
';
+ container.innerHTML = html;
+}
+
+/**
+ * Load job for editing
+ */
+async function loadJobForEdit(jobId) {
+ try {
+ if (!jobId) {
+ showMessage('No job ID provided', 'error');
+ return null;
+ }
+
+ const job = await pb.collection(jobsCollectionName).getOne(jobId);
+
+ // Update dashboard header and quick info
+ if (typeof updateDashboardInfo === 'function') {
+ updateDashboardInfo(job);
+ }
+
+ // Populate form fields
+ Object.keys(job).forEach(key => {
+ const field = document.getElementById(key);
+ if (field) {
+ if (field.type === 'checkbox') {
+ field.checked = job[key] === true;
+ } else if (field.type === 'date') {
+ if (job[key]) {
+ try {
+ const date = new Date(job[key]);
+ if (!isNaN(date.getTime())) {
+ field.value = date.toISOString().split('T')[0];
+ }
+ } catch (e) {
+ console.warn('Invalid date for field', key, job[key]);
+ }
+ }
+ } else if (field.type === 'time') {
+ if (job[key]) {
+ field.value = job[key];
+ }
+ } else {
+ field.value = job[key] || '';
+ }
+ }
+ });
+
+ return job;
+ } catch (error) {
+ console.error('Error loading job:', error);
+ showMessage('Error loading job: ' + (error.message || 'Unknown error'), 'error');
+ return null;
+ }
+}
+
+/**
+ * Save job (create or update)
+ */
+async function saveJob() {
+ try {
+ const form = document.getElementById('jobForm');
+ if (!form) {
+ showMessage('Form not found', 'error');
+ return;
+ }
+
+ const formData = new FormData(form);
+ const jobId = document.getElementById('jobId')?.value;
+
+ // Build job object
+ const jobData = {};
+ const booleanFields = ['Active', 'Flag', 'Tax_Exempt', 'Docs_Uploaded', 'Tax_Exempt_Docs_Uploaded',
+ 'Estimate_Approved', 'Estimate_Reviewed', 'Estimate_Draft', 'QB_Created',
+ 'Added_To_Calendar', 'PSwift_Uploaded', 'Voxer_Created'];
+
+ // Get all form fields
+ for (const [key, value] of formData.entries()) {
+ if (key === 'id') continue;
+
+ if (booleanFields.includes(key)) {
+ const checkbox = document.getElementById(key);
+ jobData[key] = checkbox ? checkbox.checked : false;
+ } else if (value && value.trim() !== '') {
+ jobData[key] = value.trim();
+ }
+ }
+
+ if (jobId) {
+ // Update existing job
+ await pb.collection(jobsCollectionName).update(jobId, jobData);
+ showMessage('Job updated successfully', 'success');
+ } else {
+ // Create new job
+ await pb.collection(jobsCollectionName).create(jobData);
+ showMessage('Job created successfully', 'success');
+ }
+
+ if (typeof closeJobModal === 'function') {
+ closeJobModal();
+ }
+ loadJobs(currentPage);
+ } catch (error) {
+ console.error('Error saving job:', error);
+ const errorMsg = error.message || 'Unknown error';
+ showMessage('Error saving job: ' + errorMsg, 'error');
+ }
+}
+
+/**
+ * Delete job
+ */
+async function deleteJob(jobId) {
+ if (!confirm('Are you sure you want to delete this job?')) {
+ return;
+ }
+
+ try {
+ await pb.collection(jobsCollectionName).delete(jobId);
+ showMessage('Job deleted successfully', 'success');
+ loadJobs(currentPage);
+ } catch (error) {
+ console.error('Error deleting job:', error);
+ showMessage('Error deleting job: ' + (error.message || 'Unknown error'), 'error');
+ }
+}
+
+/**
+ * Get job statistics for dashboard
+ */
+async function getJobStats() {
+ try {
+ // Ensure config is loaded before getting stats (only once)
+ if (typeof initConfig !== 'undefined') {
+ await initConfig();
+ }
+
+ const collectionName = jobsCollectionName || 'Job_Info'; // Fallback to default
+
+ // Make requests sequentially to avoid auto-cancellation
+ const totalResult = await pb.collection(collectionName).getList(1, 1);
+ const activeResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Active = true'
+ });
+ const estimatingResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Estimating"'
+ });
+ const estSentResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Est Sent"'
+ });
+ const estHoldResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Est Hold"'
+ });
+ const followUpResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Follow Up"'
+ });
+ const awardedResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Awarded"'
+ });
+ const inProgressResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "In Progress"'
+ });
+ const billedInProgressResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Billed (In Progress)"'
+ });
+ const billedClosedResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Billed (Closed)"'
+ });
+ const notAwardedResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Not Awarded"'
+ });
+ const notBiddingResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Not Bidding"'
+ });
+ const archiveResult = await pb.collection(collectionName).getList(1, 1, {
+ filter: 'Job_Status = "Archive"'
+ });
+
+ const stats = {
+ total: totalResult.totalItems || 0,
+ active: activeResult.totalItems || 0,
+ estimating: estimatingResult.totalItems || 0,
+ estSent: estSentResult.totalItems || 0,
+ estHold: estHoldResult.totalItems || 0,
+ followUp: followUpResult.totalItems || 0,
+ awarded: awardedResult.totalItems || 0,
+ inProgress: inProgressResult.totalItems || 0,
+ billedInProgress: billedInProgressResult.totalItems || 0,
+ billedClosed: billedClosedResult.totalItems || 0,
+ notAwarded: notAwardedResult.totalItems || 0,
+ notBidding: notBiddingResult.totalItems || 0,
+ archive: archiveResult.totalItems || 0
+ };
+
+ return stats;
+ } catch (error) {
+ console.error('Error getting stats:', error);
+ return {
+ total: 0,
+ active: 0,
+ estimating: 0,
+ estSent: 0,
+ estHold: 0,
+ followUp: 0,
+ awarded: 0,
+ inProgress: 0,
+ billedInProgress: 0,
+ billedClosed: 0,
+ notAwarded: 0,
+ notBidding: 0,
+ archive: 0
+ };
+ }
+}
+
+/**
+ * Load jobs for select dropdown
+ */
+async function loadJobsForSelect() {
+ try {
+ const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
+ sort: 'Job_Number'
+ });
+
+ const select = document.getElementById('noteJob');
+ if (!select) return;
+
+ select.innerHTML = '';
+ result.items.forEach(job => {
+ const option = document.createElement('option');
+ option.value = job.id;
+ option.textContent = `${job.Job_Number || job.id} - ${job.Job_Name || 'Unnamed'}`;
+ select.appendChild(option);
+ });
+ } catch (error) {
+ console.error('Error loading jobs for select:', error);
+ }
+}
+
+/**
+ * Load unique estimators and create filter buttons
+ */
+async function loadEstimatorFilters() {
+ try {
+ // Ensure config is loaded
+ await initConfig();
+
+ // Get all jobs to extract unique estimators
+ const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
+ fields: 'Estimator'
+ });
+
+ // Extract unique estimators
+ const estimators = new Set();
+ result.items.forEach(job => {
+ if (job.Estimator && job.Estimator.trim() !== '') {
+ estimators.add(job.Estimator.trim());
+ }
+ });
+
+ // Sort estimators alphabetically
+ const sortedEstimators = Array.from(estimators).sort();
+
+ // Get filter buttons container
+ const container = document.getElementById('filterButtons');
+ if (!container) return;
+
+ // Keep the "All" button and add estimator buttons
+ const allButton = container.querySelector('#filterAll');
+ container.innerHTML = '';
+ if (allButton) {
+ container.appendChild(allButton);
+ }
+
+ // Add estimator filter buttons with pastel colors
+ sortedEstimators.forEach(estimator => {
+ const button = document.createElement('button');
+ button.onclick = () => {
+ if (typeof window.setEstimatorFilter === 'function') {
+ window.setEstimatorFilter(estimator);
+ }
+ };
+ button.id = `filter-${estimator.replace(/\s+/g, '-')}`;
+
+ // Set pastel colors for specific estimators
+ let bgColor = 'bg-gray-200';
+ let textColor = 'text-gray-700';
+ let hoverColor = 'hover:bg-gray-300';
+
+ const estimatorLower = estimator.toLowerCase();
+ if (estimatorLower.includes('steve')) {
+ bgColor = 'bg-orange-200';
+ textColor = 'text-orange-800';
+ hoverColor = 'hover:bg-orange-300';
+ } else if (estimatorLower.includes('beth')) {
+ bgColor = 'bg-blue-200';
+ textColor = 'text-blue-800';
+ hoverColor = 'hover:bg-blue-300';
+ } else if (estimatorLower.includes('timothy')) {
+ bgColor = 'bg-purple-200';
+ textColor = 'text-purple-800';
+ hoverColor = 'hover:bg-purple-300';
+ }
+
+ button.className = `flex-1 sm:flex-none px-3 py-1.5 rounded-md text-xs font-medium transition focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 ${bgColor} ${textColor} ${hoverColor}`;
+ button.textContent = estimator;
+ container.appendChild(button);
+ });
+ } catch (error) {
+ console.error('Error loading estimator filters:', error);
+ }
+}
+
+/**
+ * Load unique job statuses and create filter buttons
+ */
+async function loadStatusFilters() {
+ try {
+ // Ensure config is loaded
+ await initConfig();
+
+ // Get all jobs to extract unique statuses
+ const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
+ fields: 'Job_Status'
+ });
+
+ // Extract unique statuses
+ const statuses = new Set();
+ result.items.forEach(job => {
+ if (job.Job_Status && job.Job_Status.trim() !== '') {
+ statuses.add(job.Job_Status.trim());
+ }
+ });
+
+ // Define the desired order
+ const statusOrder = [
+ 'Estimating',
+ 'Est Sent',
+ 'Est Hold',
+ 'Follow Up',
+ 'Awarded',
+ 'In Progress',
+ 'Billed (In Progress)',
+ 'Billed (Closed)',
+ 'Not Awarded',
+ 'Not Bidding',
+ 'Archive'
+ ];
+
+ // Sort statuses by the defined order, then alphabetically for any not in the list
+ const sortedStatuses = Array.from(statuses).sort((a, b) => {
+ const indexA = statusOrder.findIndex(order =>
+ a.toLowerCase() === order.toLowerCase() ||
+ a.toLowerCase().includes(order.toLowerCase()) ||
+ order.toLowerCase().includes(a.toLowerCase())
+ );
+ const indexB = statusOrder.findIndex(order =>
+ b.toLowerCase() === order.toLowerCase() ||
+ b.toLowerCase().includes(order.toLowerCase()) ||
+ order.toLowerCase().includes(b.toLowerCase())
+ );
+
+ // If both are in the order list, sort by their position
+ if (indexA !== -1 && indexB !== -1) {
+ return indexA - indexB;
+ }
+ // If only A is in the list, A comes first
+ if (indexA !== -1) return -1;
+ // If only B is in the list, B comes first
+ if (indexB !== -1) return 1;
+ // If neither is in the list, sort alphabetically
+ return a.localeCompare(b);
+ });
+
+ // Get status filter buttons container
+ const container = document.getElementById('statusFilterButtons');
+ if (!container) return;
+
+ // Keep the "All" button and add status buttons
+ const allButton = container.querySelector('#statusFilterAll');
+ container.innerHTML = '';
+ if (allButton) {
+ container.appendChild(allButton);
+ }
+
+ // Create buttons for each status with specific colors
+ sortedStatuses.forEach(status => {
+ const button = document.createElement('button');
+ button.textContent = status;
+ button.onclick = () => {
+ if (typeof window.setStatusFilter === 'function') {
+ window.setStatusFilter(status);
+ }
+ };
+
+ // Set colors based on status
+ let bgColor = 'bg-gray-200';
+ let textColor = 'text-gray-700';
+ let hoverColor = 'hover:bg-gray-300';
+
+ const statusLower = status.toLowerCase();
+
+ // Purple for Billed statuses
+ if (statusLower.includes('billed') && statusLower.includes('closed')) {
+ bgColor = 'bg-purple-200';
+ textColor = 'text-purple-800';
+ hoverColor = 'hover:bg-purple-300';
+ } else if (statusLower.includes('billed') && statusLower.includes('progress')) {
+ bgColor = 'bg-purple-200';
+ textColor = 'text-purple-800';
+ hoverColor = 'hover:bg-purple-300';
+ }
+ // Orange for Est Hold
+ else if (statusLower.includes('est hold') || statusLower === 'est hold') {
+ bgColor = 'bg-orange-200';
+ textColor = 'text-orange-800';
+ hoverColor = 'hover:bg-orange-300';
+ }
+ // Yellow for Est Sent and Follow Up
+ else if (statusLower.includes('est sent') || statusLower === 'est sent' ||
+ statusLower.includes('follow up') || statusLower === 'follow up') {
+ bgColor = 'bg-yellow-200';
+ textColor = 'text-yellow-800';
+ hoverColor = 'hover:bg-yellow-300';
+ }
+ // Blue for Estimating
+ else if (statusLower.includes('estimating') || statusLower === 'estimating') {
+ bgColor = 'bg-blue-200';
+ textColor = 'text-blue-800';
+ hoverColor = 'hover:bg-blue-300';
+ }
+ // Green for In Progress and Awarded
+ else if (statusLower.includes('in progress') || statusLower === 'in progress' ||
+ statusLower.includes('awarded') || statusLower === 'awarded') {
+ bgColor = 'bg-green-200';
+ textColor = 'text-green-800';
+ hoverColor = 'hover:bg-green-300';
+ }
+ // Red for Not Awarded and Not Bidding
+ else if (statusLower.includes('not awarded') || statusLower === 'not awarded' ||
+ statusLower.includes('not bidding') || statusLower === 'not bidding') {
+ bgColor = 'bg-red-200';
+ textColor = 'text-red-800';
+ hoverColor = 'hover:bg-red-300';
+ }
+ // Gray for Archive
+ else if (statusLower.includes('archive') || statusLower === 'archive') {
+ bgColor = 'bg-gray-200';
+ textColor = 'text-gray-700';
+ hoverColor = 'hover:bg-gray-300';
+ }
+
+ button.className = `flex-1 sm:flex-none px-3 py-1.5 rounded-md text-xs font-medium transition focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 ${bgColor} ${textColor} ${hoverColor}`;
+ container.appendChild(button);
+ });
+ } catch (error) {
+ console.error('Error loading status filters:', error);
+ }
+}
+
+/**
+ * Show message to user
+ */
+function showMessage(message, type = 'success') {
+ const container = document.getElementById('messageContainer');
+ if (!container) return;
+
+ const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700';
+
+ container.innerHTML = `
+
+ ${message}
+
+ `;
+
+ // Auto-hide after 5 seconds
+ setTimeout(() => {
+ container.innerHTML = '';
+ }, 5000);
+}
+
+/**
+ * Initialize configuration (idempotent - only runs once)
+ */
+async function initConfig() {
+ // If already initialized, return immediately
+ if (configInitialized) {
+ return;
+ }
+
+ // If currently initializing, wait for it to complete
+ if (configInitializing) {
+ while (configInitializing) {
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ return;
+ }
+
+ // Mark as initializing
+ configInitializing = true;
+
+ try {
+ if (typeof getJobsCollection === 'undefined') {
+ console.warn('getJobsCollection not available, using default collection name');
+ jobsCollectionName = 'Job_Info';
+ configInitialized = true;
+ configInitializing = false;
+ return;
+ }
+ const collectionName = await getJobsCollection();
+ jobsCollectionName = collectionName;
+ console.log('Config initialized, jobs collection:', jobsCollectionName);
+ if (typeof getItemsPerPage !== 'undefined') {
+ const itemsPerPageConfig = await getItemsPerPage();
+ itemsPerPage = itemsPerPageConfig;
+ }
+ configInitialized = true;
+ } catch (error) {
+ console.error('Error initializing config:', error);
+ // Use defaults if config fails to load
+ jobsCollectionName = jobsCollectionName || 'Job_Info';
+ configInitialized = true;
+ } finally {
+ configInitializing = false;
+ }
+}
+
+// Initialize config when script loads (but don't block)
+// Note: This will be called, but subsequent calls to initConfig() will be idempotent
+if (typeof getJobsCollection !== 'undefined') {
+ initConfig().catch(err => console.error('Config initialization error:', err));
+}
+
diff --git a/js/notes.js b/js/notes.js
new file mode 100644
index 0000000..457fa4c
--- /dev/null
+++ b/js/notes.js
@@ -0,0 +1,328 @@
+// Notes/reports operations
+
+let notesCollectionName = 'notes'; // Default, will be loaded from config
+let jobsCollectionName = 'Job_Info'; // Default, will be loaded from config
+
+/**
+ * Initialize configuration
+ */
+async function initNotesConfig() {
+ try {
+ if (typeof getNotesCollection !== 'undefined') {
+ notesCollectionName = await getNotesCollection();
+ }
+ if (typeof getJobsCollection !== 'undefined') {
+ jobsCollectionName = await getJobsCollection();
+ }
+ } catch (error) {
+ console.error('Error initializing notes config:', error);
+ // Use defaults if config fails to load
+ }
+}
+
+// Initialize config when script loads
+if (typeof getNotesCollection !== 'undefined' || typeof getJobsCollection !== 'undefined') {
+ initNotesConfig();
+}
+
+// Get note editor instance from global scope or window
+function getNoteEditor() {
+ return window.noteEditorInstance || noteEditorInstance || null;
+}
+
+/**
+ * Load notes for a specific job
+ */
+async function loadJobNotes() {
+ try {
+ const jobId = document.getElementById('jobId').value;
+ if (!jobId) {
+ document.getElementById('notesList').innerHTML = 'No job selected
';
+ return;
+ }
+
+ const job = await pb.collection(jobsCollectionName).getOne(jobId);
+ const noteRefs = job.Note_Ref || [];
+
+ if (noteRefs.length === 0) {
+ document.getElementById('notesList').innerHTML = 'No notes for this job
';
+ return;
+ }
+
+ // Fetch all notes
+ const notes = await Promise.all(
+ noteRefs.map(noteId =>
+ pb.collection(notesCollectionName).getOne(noteId).catch(() => null)
+ )
+ );
+
+ const validNotes = notes.filter(note => note !== null);
+
+ if (validNotes.length === 0) {
+ document.getElementById('notesList').innerHTML = 'No notes found
';
+ return;
+ }
+
+ displayNotes(validNotes, true);
+ } catch (error) {
+ console.error('Error loading job notes:', error);
+ document.getElementById('notesList').innerHTML = 'Error loading notes
';
+ }
+}
+
+/**
+ * Load all notes
+ */
+async function loadAllNotes() {
+ try {
+ const result = await pb.collection(notesCollectionName).getList(1, 100, {
+ sort: '-created',
+ expand: 'job'
+ });
+
+ if (result.items.length === 0) {
+ document.getElementById('loadingIndicator').classList.add('hidden');
+ document.getElementById('emptyState').classList.remove('hidden');
+ return;
+ }
+
+ document.getElementById('loadingIndicator').classList.add('hidden');
+ document.getElementById('emptyState').classList.add('hidden');
+ document.getElementById('reportsTableContainer').classList.remove('hidden');
+
+ displayNotesTable(result.items);
+ } catch (error) {
+ console.error('Error loading notes:', error);
+ document.getElementById('loadingIndicator').classList.add('hidden');
+ document.getElementById('emptyState').classList.remove('hidden');
+ }
+}
+
+/**
+ * Display notes in list format (for job modal)
+ */
+function displayNotes(notes, showDelete = false) {
+ const container = document.getElementById('notesList');
+
+ if (!notes || notes.length === 0) {
+ container.innerHTML = 'No notes
';
+ return;
+ }
+
+ container.innerHTML = notes.map(note => {
+ const date = note.created ? new Date(note.created).toLocaleString() : '-';
+ const content = note.content || '';
+ const sentiment = note.sentiment || '-';
+
+ return `
+
+
+
+
${sentiment}
+
${date}
+
+ ${showDelete ? `
` : ''}
+
+
${content}
+
+ `;
+ }).join('');
+}
+
+/**
+ * Display notes in table format (for notes page)
+ */
+function displayNotesTable(notes) {
+ const tbody = document.getElementById('notesTableBody');
+
+ tbody.innerHTML = notes.map(note => {
+ const date = note.created ? new Date(note.created).toLocaleString() : '-';
+ const content = note.content ? note.content.substring(0, 100) + '...' : '-';
+ const jobName = note.job ? (note.job.Job_Number || note.job.Job_Name || '-') : '-';
+ const sentiment = note.sentiment || '-';
+
+ return `
+
+ | ${jobName} |
+ ${content} |
+ ${sentiment} |
+ ${date} |
+
+
+
+ |
+
+ `;
+ }).join('');
+}
+
+/**
+ * Load note for editing
+ */
+async function loadNoteForEdit(noteId) {
+ try {
+ const note = await pb.collection(notesCollectionName).getOne(noteId);
+
+ document.getElementById('noteId').value = note.id;
+ document.getElementById('noteSentiment').value = note.sentiment || '';
+ document.getElementById('noteDate').value = note.date ? new Date(note.date).toISOString().slice(0, 16) : '';
+
+ if (note.job) {
+ document.getElementById('noteJob').value = note.job;
+ }
+
+ // Set Quill editor content
+ const editor = getNoteEditor();
+ if (editor && note.content) {
+ editor.root.innerHTML = note.content;
+ }
+
+ // Load jobs for select if not already loaded
+ await loadJobsForSelect();
+ } catch (error) {
+ console.error('Error loading note:', error);
+ showMessage('Error loading note: ' + (error.message || 'Unknown error'), 'error');
+ }
+}
+
+/**
+ * Save note (create or update)
+ */
+async function saveNote() {
+ try {
+ const noteId = document.getElementById('noteId')?.value;
+ const jobId = document.getElementById('noteJob')?.value || document.getElementById('jobId')?.value;
+ const editor = getNoteEditor();
+ const content = document.getElementById('noteContent')?.value || editor?.root.innerHTML || '';
+ const sentiment = document.getElementById('noteSentiment')?.value || '';
+ const date = document.getElementById('noteDate')?.value || new Date().toISOString();
+
+ if (!jobId && !noteId) {
+ showMessage('Please select a job', 'error');
+ return;
+ }
+
+ const noteData = {
+ content: content,
+ sentiment: sentiment,
+ date: date
+ };
+
+ let savedNote;
+
+ if (noteId) {
+ // Update existing note
+ savedNote = await pb.collection(notesCollectionName).update(noteId, noteData);
+ showMessage('Note updated successfully', 'success');
+ } else {
+ // Create new note
+ savedNote = await pb.collection(notesCollectionName).create(noteData);
+ showMessage('Note created successfully', 'success');
+
+ // Link note to job
+ if (jobId) {
+ await linkNoteToJob(savedNote.id, jobId);
+ }
+ }
+
+ // Close modal and reload
+ if (document.getElementById('noteModal')) {
+ closeNoteModal();
+ loadAllNotes();
+ } else {
+ // In job modal
+ cancelAddNote();
+ loadJobNotes();
+ }
+ } catch (error) {
+ console.error('Error saving note:', error);
+ showMessage('Error saving note: ' + (error.message || 'Unknown error'), 'error');
+ }
+}
+
+/**
+ * Link note to job
+ */
+async function linkNoteToJob(noteId, jobId) {
+ try {
+ const job = await pb.collection(jobsCollectionName).getOne(jobId);
+ const noteRefs = job.Note_Ref || [];
+
+ if (!noteRefs.includes(noteId)) {
+ noteRefs.push(noteId);
+ await pb.collection(jobsCollectionName).update(jobId, {
+ Note_Ref: noteRefs
+ });
+ }
+ } catch (error) {
+ console.error('Error linking note to job:', error);
+ }
+}
+
+/**
+ * Delete note
+ */
+async function deleteNote(noteId) {
+ if (!confirm('Are you sure you want to delete this note?')) {
+ return;
+ }
+
+ try {
+ await pb.collection(notesCollectionName).delete(noteId);
+ showMessage('Note deleted successfully', 'success');
+ loadAllNotes();
+ } catch (error) {
+ console.error('Error deleting note:', error);
+ showMessage('Error deleting note: ' + (error.message || 'Unknown error'), 'error');
+ }
+}
+
+/**
+ * Delete note from job (unlink)
+ */
+async function deleteNoteFromJob(noteId) {
+ if (!confirm('Are you sure you want to delete this note?')) {
+ return;
+ }
+
+ try {
+ const jobId = document.getElementById('jobId').value;
+ if (jobId) {
+ const job = await pb.collection(jobsCollectionName).getOne(jobId);
+ const noteRefs = (job.Note_Ref || []).filter(id => id !== noteId);
+ await pb.collection(jobsCollectionName).update(jobId, {
+ Note_Ref: noteRefs
+ });
+ }
+
+ await pb.collection(notesCollectionName).delete(noteId);
+ showMessage('Note deleted successfully', 'success');
+ loadJobNotes();
+ } catch (error) {
+ console.error('Error deleting note:', error);
+ showMessage('Error deleting note: ' + (error.message || 'Unknown error'), 'error');
+ }
+}
+
+/**
+ * Show add note form (in job modal)
+ */
+function showAddNoteForm() {
+ document.getElementById('addNoteForm').classList.remove('hidden');
+ const editor = getNoteEditor();
+ if (editor) {
+ editor.setContents([]);
+ }
+}
+
+/**
+ * Cancel add note form
+ */
+function cancelAddNote() {
+ document.getElementById('addNoteForm').classList.add('hidden');
+ const editor = getNoteEditor();
+ if (editor) {
+ editor.setContents([]);
+ }
+}
+
diff --git a/js/pocketbase.js b/js/pocketbase.js
new file mode 100644
index 0000000..90f1dfb
--- /dev/null
+++ b/js/pocketbase.js
@@ -0,0 +1,23 @@
+// PocketBase client initialization
+// Update this URL to match your PocketBase instance
+const POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
+
+// Initialize PocketBase client
+const pb = new PocketBase(POCKETBASE_URL);
+
+// Handle connection errors
+pb.beforeSend = function (url, options) {
+ console.log('PocketBase request:', url, options);
+ return { url, options };
+};
+
+// Handle auth state changes
+pb.authStore.onChange((token, model) => {
+ console.log('Auth state changed:', token ? 'authenticated' : 'not authenticated');
+});
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = pb;
+}
+
diff --git a/notes.html b/notes.html
new file mode 100644
index 0000000..a88f385
--- /dev/null
+++ b/notes.html
@@ -0,0 +1,233 @@
+
+
+
+
+
+ Job Tracking - Notes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Job Notes
+
Manage job notes and reports
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Job |
+ Note |
+ Sentiment |
+ Date |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
No notes
+
Get started by creating a new note.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7e18627
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "job-tracking-system",
+ "version": "1.0.0",
+ "description": "Job Tracking System - A web application for managing construction/contracting jobs",
+ "type": "module",
+ "scripts": {
+ "dev": "bun run server.js"
+ },
+ "keywords": [
+ "job-tracking",
+ "pocketbase",
+ "construction"
+ ],
+ "author": "",
+ "license": "MIT"
+}
+
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..47f1158
--- /dev/null
+++ b/server.js
@@ -0,0 +1,61 @@
+// Simple Bun server for development
+const server = Bun.serve({
+ port: 3075,
+ async fetch(request) {
+ const url = new URL(request.url);
+ let pathname = url.pathname;
+
+ // Default to index.html for root
+ if (pathname === '/') {
+ pathname = '/index.html';
+ }
+
+ // Try to serve the file
+ const file = Bun.file('.' + pathname);
+
+ // If file doesn't exist, try with .html extension
+ if (!(await file.exists())) {
+ const htmlFile = Bun.file('.' + pathname + '.html');
+ if (await htmlFile.exists()) {
+ return new Response(htmlFile, {
+ headers: {
+ 'Content-Type': getContentType(pathname + '.html'),
+ },
+ });
+ }
+ }
+
+ // Serve the file if it exists
+ if (await file.exists()) {
+ return new Response(file, {
+ headers: {
+ 'Content-Type': getContentType(pathname),
+ },
+ });
+ }
+
+ // 404 for not found
+ return new Response('Not Found', { status: 404 });
+ },
+});
+
+function getContentType(pathname) {
+ const ext = pathname.split('.').pop()?.toLowerCase();
+ const types = {
+ 'html': 'text/html',
+ 'css': 'text/css',
+ 'js': 'application/javascript',
+ 'json': 'application/json',
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'svg': 'image/svg+xml',
+ 'ico': 'image/x-icon',
+ };
+ return types[ext] || 'text/plain';
+}
+
+console.log(`🚀 Server running at http://localhost:${server.port}`);
+console.log(`📁 Serving files from: ${process.cwd()}`);
+