Files
Jobs-List/estimatortable.html
aewing db3d8df6cc feat: add estimator table with frozen columns, settings UI, and dynamic column configuration
- Created estimatortable.html with 47 columns displaying Job_Info_TestEnv data
- Implemented frozen first two columns (Job_Number, Job_Full_Name) with sticky positioning
- Implemented frozen header row with proper z-index layering
- Added proper scrolling (vertical and horizontal within table container)
- Applied .cursorrules formatting: dates in Central Time (MM/dd/yyyy), booleans as checkboxes
- Implemented calculated fields: Due_Date_Counter with business logic, Active field read-only
- Made boolean fields editable with PocketBase updates (except Active)
- Created appsettings.html for comprehensive table configuration
- Added global settings: row height, font size, striped rows
- Added per-column settings: width, visibility, read-only status
- Added dropdown choice configuration with color pickers and font styles for Job_Status
- Settings save/load from app_preferences_settings collection
- Fixed column position mapping with COLUMN_ORDER array for correct visibility and width application
- Fixed horizontal scroll by calculating total table width dynamically
- Added server routes for /estimatortable.html and /appsettings.html
- Updated port to 6500 with improved console output
2025-12-22 05:48:18 +00:00

756 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Estimator Table</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4c51bf',
secondary: '#5b21b6',
}
}
}
}
</script>
<style>
/* Fixed layout - no outer scroll */
body {
display: flex;
flex-direction: column;
}
/* Container layout - keep header stationary */
#tableContainer {
display: flex;
flex-direction: column;
height: 100vh;
padding: 1rem;
padding-top: 3.5rem; /* Space for user display */
}
/* Fixed header section */
.header-section {
flex-shrink: 0;
margin-bottom: 1rem;
}
/* Scrollable table area with always-visible scrollbars */
.table-container {
position: relative;
overflow: scroll; /* Always show scrollbars */
flex: 1;
border: 1px solid #e5e7eb;
background: white;
border-radius: 0.5rem;
min-height: 0; /* Critical for flex child scrolling */
}
/* Force scrollbars to always be visible */
.table-container::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.table-container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #555;
}
.data-table {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed; /* Fixed table layout for consistent column widths */
}
/* Fixed column widths */
.data-table th,
.data-table td {
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
}
/* Freeze header row */
.data-table thead th {
position: sticky;
top: 0;
z-index: 20;
background: linear-gradient(to bottom right, #4c51bf, #5b21b6);
color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
border-right: 1px solid rgba(255,255,255,0.2);
}
/* Freeze first column (Job_Number) - fixed 120px width */
.data-table th:nth-child(1),
.data-table td:nth-child(1) {
position: sticky;
left: 0;
z-index: 15;
background: white;
border-right: 2px solid #4c51bf !important;
}
/* First column header - highest z-index */
.data-table thead th:nth-child(1) {
z-index: 35;
background: linear-gradient(to bottom right, #4c51bf, #5b21b6);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
border-right: 2px solid rgba(255,255,255,0.3) !important;
}
/* Freeze second column (Job_Full_Name) - fixed 250px width */
.data-table th:nth-child(2),
.data-table td:nth-child(2) {
position: sticky;
left: 120px;
z-index: 15;
background: white;
border-right: 2px solid #4c51bf !important;
}
/* Second column header - highest z-index */
.data-table thead th:nth-child(2) {
z-index: 35;
background: linear-gradient(to bottom right, #4c51bf, #5b21b6);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
border-right: 2px solid rgba(255,255,255,0.3) !important;
}
/* Alternating row colors - maintain on frozen columns */
.data-table tbody tr:nth-child(even) td {
background: #f9fafb;
}
.data-table tbody tr:nth-child(even) td:nth-child(1),
.data-table tbody tr:nth-child(even) td:nth-child(2) {
background: #f9fafb;
}
.data-table tbody tr:hover td {
background: #fef3c7;
}
.data-table tbody tr:hover td:nth-child(1),
.data-table tbody tr:hover td:nth-child(2) {
background: #fef3c7;
}
/* Prominent checkbox styling */
.checkbox-cell input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: default;
accent-color: #000;
border: 2px solid #000;
}
/* Override disabled appearance - keep checkboxes looking active */
.checkbox-cell input[type="checkbox"]:disabled {
opacity: 1;
cursor: default;
}
</style>
</head>
<body class="h-screen overflow-hidden bg-gradient-to-br from-primary to-secondary font-sans">
<!-- Top-right display name and settings -->
<div id="userDisplay" class="hidden absolute top-4 right-4 flex items-center space-x-3">
<span id="userDisplayName" class="text-white text-sm font-semibold drop-shadow"></span>
<button id="settingsBtn" class="text-white hover:text-gray-200 transition-colors" title="Settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
<!-- Login Container -->
<div id="loginContainer" class="hidden max-w-md mx-auto bg-white rounded-2xl shadow-2xl p-8 mt-20">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Estimator Table</h1>
<p class="text-gray-600">Job Information</p>
</div>
<div class="space-y-6">
<p class="text-gray-700 text-center">Sign in with your Microsoft account to access the table</p>
<button type="button" id="loginBtn" class="w-full py-3 px-4 bg-gradient-to-br from-primary to-secondary text-white font-semibold rounded-lg hover:opacity-95 transition-opacity shadow-lg hover:shadow-xl">
Login with Microsoft
</button>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="hidden text-center text-white">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
<p class="mt-4">Loading data...</p>
</div>
<!-- Table Container -->
<div id="tableContainer" class="hidden">
<div class="header-section bg-white rounded-lg shadow-2xl p-6">
<h1 class="text-3xl font-bold text-gray-800">Estimator Job Table</h1>
<p class="text-gray-600 mt-1">Scroll horizontally and vertically • First two columns are frozen</p>
</div>
<div class="bg-white rounded-lg shadow-2xl p-4" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
<div class="table-container">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Number</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Full Name</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Active</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Added To Calendar</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Attachment List</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Company Client</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Contact Person</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Docs Uploaded</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Due Date</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Due Date Counter</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Due Date Source</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Due Time</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Email</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Estimate Approved</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Estimate Draft</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Estimate Reviewed</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Estimator</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Flag</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Has Attachments</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Address</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Codes</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Division</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Folder Link</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Name</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Number Parent</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job QB Link</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Status</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Job Type</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Note Ref</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Notes</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Office Rep</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">PSwift Uploaded</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Phone Number</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Project Manager</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">QB Created</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Schedule Confidence</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Start Date</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Submission Date</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Tax Exempt</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Tax Exempt Docs Uploaded</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Voxer Created</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Voxer Link</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Last Note At</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Latest Note Summary</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Note Count</th>
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">Note Thread Text</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Data will be inserted here -->
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
<script>
// Initialize PocketBase client
const pb = new PocketBase('https://pocketbase.ccllc.pro');
// Column name to position mapping (based on actual table order)
const COLUMN_ORDER = [
'Job_Number', 'Job_Full_Name', 'Active', 'Added_To_Calendar', 'Attachment_List',
'Company_Client', 'Contact_Person', 'Docs_Uploaded', 'Due_Date', 'Due_Date_Counter',
'Due_Date_Source', 'Due_Time', 'Email', 'Estimate_Approved', 'Estimate_Draft',
'Estimate_Reviewed', 'Estimator', 'Flag', 'Has_Attachments', 'Job_Address',
'Job_Codes', 'Job_Division', 'Job_Folder_Link', 'Job_Name', 'Job_Number_Parent',
'Job_QB_Link', 'Job_Status', 'Job_Type', 'Note_Ref', 'Notes',
'Office_Rep', 'PSwift_Uploaded', 'Phone_Number', 'Project_Manager', 'QB_Created',
'Schedule_Confidence', 'Start_Date', 'Submission_Date', 'Tax_Exempt', 'Tax_Exempt_Docs_Uploaded',
'Voxer_Created', 'Voxer_Link', 'last_note_at', 'latest_note_summary', 'note_count',
'note_thread_text', 'created'
];
const loginContainer = document.getElementById('loginContainer');
const tableContainer = document.getElementById('tableContainer');
const loadingIndicator = document.getElementById('loadingIndicator');
const userDisplay = document.getElementById('userDisplay');
const userDisplayName = document.getElementById('userDisplayName');
const loginBtn = document.getElementById('loginBtn');
const tableBody = document.getElementById('tableBody');
const APP_NAME = 'Estimator Job List';
let appSettings = null;
// Load app settings from PocketBase
async function loadAppSettings() {
try {
const records = await pb.collection('app_preferences_settings').getFullList({
filter: `app_name = "${APP_NAME}"`
});
if (records.length > 0) {
const settingsData = records[0].app_json_prefs;
if (settingsData) {
appSettings = typeof settingsData === 'string' ? JSON.parse(settingsData) : settingsData;
console.log('Loaded app settings:', appSettings);
applyGlobalSettings();
}
}
} catch (error) {
console.error('Error loading app settings:', error);
}
}
// Apply global settings to table
function applyGlobalSettings() {
if (!appSettings) return;
// Remove any existing dynamic styles first
const existingStyle = document.getElementById('dynamic-column-styles');
if (existingStyle) {
existingStyle.remove();
}
let styleContent = '';
// Apply row height
if (appSettings.rowHeight) {
styleContent += `.data-table tbody tr { height: ${appSettings.rowHeight}px; }\n`;
}
// Apply font size
if (appSettings.fontSize) {
styleContent += `.data-table { font-size: ${appSettings.fontSize}px; }\n`;
}
// Apply striped rows setting
if (appSettings.stripedRows === false) {
styleContent += `.data-table tbody tr:nth-child(even) td { background: white !important; }\n`;
}
// Apply column widths and visibility dynamically
if (appSettings.columns) {
const col1Settings = getColumnSettings('Job_Number');
const col2Settings = getColumnSettings('Job_Full_Name');
const col1Width = col1Settings?.width || 120;
const col2Width = col2Settings?.width || 250;
console.log('Applying settings:', { col1Width, col2Width, columns: appSettings.columns.length });
// Update sticky positions and widths for first two columns
styleContent += `
.data-table th:nth-child(1), .data-table td:nth-child(1) {
width: ${col1Width}px !important;
min-width: ${col1Width}px !important;
max-width: ${col1Width}px !important;
}
.data-table th:nth-child(2), .data-table td:nth-child(2) {
left: ${col1Width}px !important;
width: ${col2Width}px !important;
min-width: ${col2Width}px !important;
max-width: ${col2Width}px !important;
}
`;
// Calculate total width and apply settings to all columns
let totalWidth = 0;
appSettings.columns.forEach((col) => {
// Find the actual position in the table
const colPosition = COLUMN_ORDER.indexOf(col.name) + 1;
if (colPosition === 0) {
console.warn(`Column ${col.name} not found in COLUMN_ORDER`);
return;
}
// Apply width for all columns
if (col.width) {
styleContent += `
.data-table th:nth-child(${colPosition}),
.data-table td:nth-child(${colPosition}) {
width: ${col.width}px !important;
min-width: ${col.width}px !important;
max-width: ${col.width}px !important;
}
`;
}
// Hide columns marked as not visible
if (col.visible === false) {
console.log(`Hiding column ${col.name} at position ${colPosition}`);
styleContent += `
.data-table th:nth-child(${colPosition}),
.data-table td:nth-child(${colPosition}) {
display: none !important;
}
`;
} else {
// Add to total width for visible columns
totalWidth += col.width || 150;
}
});
// Ensure table is wide enough to trigger horizontal scroll
console.log(`Total table width: ${totalWidth}px`);
styleContent += `
.data-table {
width: ${totalWidth}px !important;
min-width: ${totalWidth}px !important;
}
`;
}
const style = document.createElement('style');
style.id = 'dynamic-column-styles';
style.textContent = styleContent;
document.head.appendChild(style);
}
// Get column settings
function getColumnSettings(columnName) {
if (!appSettings || !appSettings.columns) return null;
return appSettings.columns.find(col => col.name === columnName);
}
// Get choice styling for a value
function getChoiceStyling(columnName, value) {
const colSettings = getColumnSettings(columnName);
if (!colSettings || !colSettings.choices) return null;
const choice = colSettings.choices.find(c => c.value === value);
return choice || null;
}
// Check if user is already authenticated
async function checkAuth() {
if (pb.authStore.isValid) {
// Already logged in
await loadAppSettings();
showTable();
await loadTableData();
} else {
// Need to login
loginContainer.classList.remove('hidden');
}
}
// Login with Microsoft
loginBtn.addEventListener('click', async () => {
try {
loginBtn.disabled = true;
loginBtn.textContent = 'Logging in...';
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
await loadAppSettings();
showTable();
await loadTableData();
} catch (error) {
console.error('Login failed:', error);
alert('Login failed. Please try again.');
loginBtn.disabled = false;
loginBtn.textContent = 'Login with Microsoft';
}
});
function showTable() {
loginContainer.classList.add('hidden');
tableContainer.classList.remove('hidden');
if (pb.authStore.model) {
userDisplay.classList.remove('hidden');
userDisplayName.textContent = pb.authStore.model.name || pb.authStore.model.email || 'User';
}
}
// Settings button handler
document.addEventListener('click', (e) => {
if (e.target.closest('#settingsBtn')) {
window.location.href = '/appsettings.html';
}
});
// Format date from UTC to Central Time (MM/dd/yyyy)
function formatDate(dateString, notesField = null) {
// Check for sentinel date (null date representation)
if (!dateString || dateString === '1900-01-01T00:00:00.000Z' || dateString.startsWith('1900-01-01')) {
// If there's a related notes field with content, show "See Notes"
if (notesField && notesField.trim()) {
return 'See Notes';
}
return '';
}
try {
const date = new Date(dateString);
// Format in Central Time
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Chicago',
month: '2-digit',
day: '2-digit',
year: 'numeric'
});
return formatter.format(date);
} catch (error) {
console.error('Date format error:', error, dateString);
return '';
}
}
// Format datetime with time (MM/dd/yyyy h:mm am/pm)
function formatDateTime(dateString) {
if (!dateString || dateString === '1900-01-01T00:00:00.000Z' || dateString.startsWith('1900-01-01')) {
return '';
}
try {
const date = new Date(dateString);
const dateFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Chicago',
month: '2-digit',
day: '2-digit',
year: 'numeric'
});
const timeFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Chicago',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
return `${dateFormatter.format(date)} ${timeFormatter.format(date)}`;
} catch (error) {
console.error('DateTime format error:', error, dateString);
return '';
}
}
// Calculate Due_Date_Counter based on .cursorrules logic
function calculateDueDateCounter(record) {
const jobStatus = record.Job_Status;
const dueDate = record.Due_Date;
const dueDateASAP = record.Due_Date_ASAP; // Will be undefined until schema updated
// 1. If Job_Status ≠ "Estimating" → "N/A"
if (jobStatus !== 'Estimating') {
return 'N/A';
}
// 2. If Due_Date_ASAP = true → "ASAP"
if (dueDateASAP === true) {
return 'ASAP';
}
// 3. If Due_Date is blank/sentinel → blank/empty
if (!dueDate || dueDate === '1900-01-01T00:00:00.000Z' || dueDate.startsWith('1900-01-01')) {
return '';
}
try {
const due = new Date(dueDate);
const today = new Date();
// Convert both to Central Time midnight for comparison
const dueMidnight = new Date(due.toLocaleString('en-US', { timeZone: 'America/Chicago' }));
dueMidnight.setHours(0, 0, 0, 0);
const todayMidnight = new Date(today.toLocaleString('en-US', { timeZone: 'America/Chicago' }));
todayMidnight.setHours(0, 0, 0, 0);
const diffTime = dueMidnight - todayMidnight;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// 4. If Due_Date < Today → "Passed due"
if (diffDays < 0) {
return 'Passed due';
}
// 5. If Due_Date = Today → "Due today"
if (diffDays === 0) {
return 'Due today';
}
// 6. Else → "[X] day" or "[X] days"
return `${diffDays} ${diffDays === 1 ? 'day' : 'days'}`;
} catch (error) {
console.error('Due date counter calculation error:', error);
return '';
}
}
// Format boolean values as prominent checkboxes (read-only)
function formatBoolean(value) {
return value ? '✓' : '';
}
// Format Active field as read-only checkbox with alert
function formatActiveCheckbox(value, recordId) {
return `<div class="checkbox-cell"><input type="checkbox" ${value ? 'checked' : ''} data-field="Active" data-readonly="true" data-record-id="${recordId}"></div>`;
}
// Format editable boolean as checkbox element
function formatEditableCheckbox(value, fieldName, recordId) {
return `<div class="checkbox-cell"><input type="checkbox" ${value ? 'checked' : ''} data-field="${fieldName}" data-record-id="${recordId}"></div>`;
}
// Load table data from PocketBase
async function loadTableData() {
try {
loadingIndicator.classList.remove('hidden');
// Fetch all records from Job_Info_TestEnv collection
const records = await pb.collection('Job_Info_TestEnv').getFullList({
sort: 'Job_Number',
});
// Clear existing rows
tableBody.innerHTML = '';
// Populate table
records.forEach(record => {
const row = document.createElement('tr');
row.className = 'border-b border-gray-200';
// Helper to get cell style based on column settings and value
const getCellStyle = (columnName, value) => {
let style = '';
const colSettings = getColumnSettings(columnName);
if (colSettings) {
// Check for choice-based styling
if (value && colSettings.choices && colSettings.choices.length > 0) {
const choice = colSettings.choices.find(c => c.value === value);
if (choice) {
// Use alternating colors based on row index
const rowIndex = Array.from(tableBody.children).length;
const bgColor = rowIndex % 2 === 0 ? choice.bgColor : choice.bgColorAlt;
style += `background-color: ${bgColor} !important;`;
if (choice.fontStyle === 'bold') {
style += 'font-weight: bold;';
} else if (choice.fontStyle === 'italic') {
style += 'font-style: italic;';
}
}
}
}
return style ? `style="${style}"` : '';
};
row.innerHTML = `
<td class="px-4 py-3 border-r border-gray-200 whitespace-nowrap font-medium" ${getCellStyle('Job_Number', record.Job_Number)}>${record.Job_Number || ''}</td>
<td class="px-4 py-3 border-r border-gray-200 whitespace-nowrap" ${getCellStyle('Job_Full_Name', record.Job_Full_Name)}>${record.Job_Full_Name || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Active', record.Active)}>${formatActiveCheckbox(record.Active, record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Added_To_Calendar', record.Added_To_Calendar)}>${formatEditableCheckbox(record.Added_To_Calendar, 'Added_To_Calendar', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Attachment_List', record.Attachment_List)}>${record.Attachment_List || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Company_Client', record.Company_Client)}>${record.Company_Client || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Contact_Person', record.Contact_Person)}>${record.Contact_Person || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Docs_Uploaded', record.Docs_Uploaded)}>${formatEditableCheckbox(record.Docs_Uploaded, 'Docs_Uploaded', record.id)}</td>
<td class="px-4 py-3 whitespace-nowrap" ${getCellStyle('Due_Date', record.Due_Date)}>${formatDate(record.Due_Date, record.Due_Date_Notes)}</td>
<td class="px-4 py-3" ${getCellStyle('Due_Date_Counter', record.Due_Date_Counter)}>${calculateDueDateCounter(record)}</td>
<td class="px-4 py-3" ${getCellStyle('Due_Date_Source', record.Due_Date_Source)}>${record.Due_Date_Source || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Due_Time', record.Due_Time)}>${record.Due_Time || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Email', record.Email)}>${record.Email || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Estimate_Approved', record.Estimate_Approved)}>${formatEditableCheckbox(record.Estimate_Approved, 'Estimate_Approved', record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Estimate_Draft', record.Estimate_Draft)}>${formatEditableCheckbox(record.Estimate_Draft, 'Estimate_Draft', record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Estimate_Reviewed', record.Estimate_Reviewed)}>${formatEditableCheckbox(record.Estimate_Reviewed, 'Estimate_Reviewed', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Estimator', record.Estimator)}>${record.Estimator || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Flag', record.Flag)}>${formatEditableCheckbox(record.Flag, 'Flag', record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Has_Attachments', record.Has_Attachments)}>${formatEditableCheckbox(record.Has_Attachments, 'Has_Attachments', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Address', record.Job_Address)}>${record.Job_Address || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Codes', record.Job_Codes)}>${record.Job_Codes || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Division', record.Job_Division)}>${record.Job_Division || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Folder_Link', record.Job_Folder_Link)}>${record.Job_Folder_Link || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Name', record.Job_Name)}>${record.Job_Name || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Number_Parent', record.Job_Number_Parent)}>${record.Job_Number_Parent || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_QB_Link', record.Job_QB_Link)}>${record.Job_QB_Link || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Status', record.Job_Status)}>${record.Job_Status || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Job_Type', record.Job_Type)}>${record.Job_Type || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Note_Ref', record.Note_Ref)}>${record.Note_Ref || ''}</td>
<td class="px-4 py-3 max-w-xs truncate" title="${record.Notes || ''}" ${getCellStyle('Notes', record.Notes)}>${record.Notes || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Office_Rep', record.Office_Rep)}>${record.Office_Rep || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('PSwift_Uploaded', record.PSwift_Uploaded)}>${formatEditableCheckbox(record.PSwift_Uploaded, 'PSwift_Uploaded', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Phone_Number', record.Phone_Number)}>${record.Phone_Number || ''}</td>
<td class="px-4 py-3" ${getCellStyle('Project_Manager', record.Project_Manager)}>${record.Project_Manager || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('QB_Created', record.QB_Created)}>${formatEditableCheckbox(record.QB_Created, 'QB_Created', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Schedule_Confidence', record.Schedule_Confidence)}>${record.Schedule_Confidence || ''}</td>
<td class="px-4 py-3 whitespace-nowrap" ${getCellStyle('Start_Date', record.Start_Date)}>${formatDate(record.Start_Date, record.Start_Date_Notes)}</td>
<td class="px-4 py-3 whitespace-nowrap" ${getCellStyle('Submission_Date', record.Submission_Date)}>${formatDate(record.Submission_Date)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Tax_Exempt', record.Tax_Exempt)}>${formatEditableCheckbox(record.Tax_Exempt, 'Tax_Exempt', record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Tax_Exempt_Docs_Uploaded', record.Tax_Exempt_Docs_Uploaded)}>${formatEditableCheckbox(record.Tax_Exempt_Docs_Uploaded, 'Tax_Exempt_Docs_Uploaded', record.id)}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('Voxer_Created', record.Voxer_Created)}>${formatEditableCheckbox(record.Voxer_Created, 'Voxer_Created', record.id)}</td>
<td class="px-4 py-3" ${getCellStyle('Voxer_Link', record.Voxer_Link)}>${record.Voxer_Link || ''}</td>
<td class="px-4 py-3 whitespace-nowrap" ${getCellStyle('last_note_at', record.last_note_at)}>${formatDateTime(record.last_note_at)}</td>
<td class="px-4 py-3 max-w-xs truncate" title="${record.latest_note_summary || ''}" ${getCellStyle('latest_note_summary', record.latest_note_summary)}>${record.latest_note_summary || ''}</td>
<td class="px-4 py-3 text-center" ${getCellStyle('note_count', record.note_count)}>${record.note_count || ''}</td>
<td class="px-4 py-3 max-w-xs truncate" title="${record.note_thread_text || ''}" ${getCellStyle('note_thread_text', record.note_thread_text)}>${record.note_thread_text || ''}</td>
`;
tableBody.appendChild(row);
});
loadingIndicator.classList.add('hidden');
} catch (error) {
console.error('Error loading table data:', error);
loadingIndicator.classList.add('hidden');
alert('Error loading data. Please refresh the page.');
}
}
// Handle checkbox changes
document.addEventListener('change', async (e) => {
if (e.target.type === 'checkbox' && e.target.dataset.field) {
const checkbox = e.target;
const fieldName = checkbox.dataset.field;
const recordId = checkbox.dataset.recordId;
const isReadOnly = checkbox.dataset.readonly === 'true';
// Handle read-only Active field
if (isReadOnly && fieldName === 'Active') {
// Revert the checkbox state
checkbox.checked = !checkbox.checked;
alert('Active status is calculated automatically by selected Job Status values.');
return;
}
// Update PocketBase for editable fields
const newValue = checkbox.checked;
try {
await pb.collection('Job_Info_TestEnv').update(recordId, {
[fieldName]: newValue
});
console.log(`Updated ${fieldName} to ${newValue} for record ${recordId}`);
} catch (error) {
console.error('Error updating field:', error);
// Revert checkbox on error
checkbox.checked = !checkbox.checked;
alert('Failed to update field. Please try again.');
}
}
});
// Initialize
checkAuth();
</script>
</body>
</html>