diff --git a/appsettings.html b/appsettings.html
index d2265d9..bc4a44c 100644
--- a/appsettings.html
+++ b/appsettings.html
@@ -82,6 +82,7 @@
+
Fixed height for all data rows
@@ -91,11 +92,71 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -215,6 +276,13 @@
currentSettings.rowHeight = parseInt(document.getElementById('rowHeight').value);
currentSettings.fontSize = parseInt(document.getElementById('fontSize').value);
currentSettings.stripedRows = document.getElementById('stripedRows').checked;
+ currentSettings.fixedRowHeight = document.getElementById('fixedRowHeight').checked;
+ currentSettings.cellVerticalAlign = document.getElementById('cellVerticalAlign').value;
+ currentSettings.headerVerticalAlign = document.getElementById('headerVerticalAlign').value;
+ currentSettings.borderColor = document.getElementById('borderColor').value;
+ currentSettings.borderWidth = parseInt(document.getElementById('borderWidth').value);
+ currentSettings.showRowBorders = document.getElementById('showRowBorders').checked;
+ currentSettings.showColumnBorders = document.getElementById('showColumnBorders').checked;
}
async function saveSettings(isAuto = false) {
@@ -349,6 +417,14 @@
document.getElementById('rowHeight').value = currentSettings.rowHeight || 40;
document.getElementById('fontSize').value = currentSettings.fontSize || 14;
document.getElementById('stripedRows').checked = currentSettings.stripedRows !== false;
+ document.getElementById('fixedRowHeight').checked = currentSettings.fixedRowHeight !== false;
+ document.getElementById('cellVerticalAlign').value = currentSettings.cellVerticalAlign || 'middle';
+ document.getElementById('headerVerticalAlign').value = currentSettings.headerVerticalAlign || 'middle';
+ document.getElementById('borderColor').value = currentSettings.borderColor || '#e5e7eb';
+ document.getElementById('borderColorText').value = currentSettings.borderColor || '#e5e7eb';
+ document.getElementById('borderWidth').value = currentSettings.borderWidth || 1;
+ document.getElementById('showRowBorders').checked = currentSettings.showRowBorders !== false;
+ document.getElementById('showColumnBorders').checked = currentSettings.showColumnBorders !== false;
// Column settings
const columnsList = document.getElementById('columnsList');
@@ -388,10 +464,70 @@
-
+
+
+
Read-only
+
+
+ Wrap header
+
+
+
+ Hover tooltip
+
+
+
+
+
+
+ Boolean Value Formatting (for checkboxes)
+
+
@@ -604,6 +740,18 @@
document.getElementById('closeBtn').addEventListener('click', () => {
window.location.href = '/estimatortable.html';
});
+
+ // Sync border color picker with text input
+ document.getElementById('borderColor').addEventListener('input', (e) => {
+ document.getElementById('borderColorText').value = e.target.value;
+ });
+
+ document.getElementById('borderColorText').addEventListener('input', (e) => {
+ const value = e.target.value;
+ if (/^#[0-9A-F]{6}$/i.test(value)) {
+ document.getElementById('borderColor').value = value;
+ }
+ });
// Initialize
if (pb.authStore.isValid) {
diff --git a/estimatortable.html b/estimatortable.html
index 09f5635..e818e1b 100644
--- a/estimatortable.html
+++ b/estimatortable.html
@@ -85,6 +85,24 @@
border-bottom: 1px solid #e5e7eb;
}
+ .data-table td {
+ white-space: nowrap;
+ }
+
+ /* Enforce fixed row heights */
+ .data-table tbody tr {
+ height: 40px;
+ max-height: 40px;
+ overflow: hidden;
+ }
+
+ .data-table tbody td {
+ height: 40px;
+ max-height: 40px;
+ overflow: hidden;
+ vertical-align: middle;
+ }
+
/* Freeze header row */
.data-table thead th {
position: sticky;
@@ -94,6 +112,7 @@
color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
border-right: 1px solid rgba(255,255,255,0.2);
+ padding: 12px 16px;
}
/* Freeze first column (Job_Number) - fixed 120px width */
@@ -152,12 +171,21 @@
}
/* Prominent checkbox styling */
+ .checkbox-cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ }
+
.checkbox-cell input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: default;
accent-color: #000;
border: 2px solid #000;
+ margin: 0;
}
/* Override disabled appearance - keep checkboxes looking active */
@@ -298,7 +326,7 @@
- v1.0.0-alpha4
+ v1.0.0-alpha5
@@ -368,6 +396,10 @@
}
function prettyLabel(name) {
+ // Special case for Tax_Exempt_Docs_Uploaded
+ if (name === 'Tax_Exempt_Docs_Uploaded') {
+ return 'Tax Exempt Uploads?';
+ }
return name.replace(/_/g, ' ');
}
@@ -456,9 +488,9 @@
console.log('App settings updated via realtime', { action: e.action });
applyGlobalSettings();
scheduleTableReload();
- });
+ }, { /* Silently handle connection errors */ });
} catch (err) {
- console.error('Failed to start settings watch', err);
+ // Silently ignore realtime connection errors (QUIC protocol issues are cosmetic)
}
}
@@ -471,9 +503,12 @@
}
const visibleColumns = getVisibleColumns();
console.log('Rendering header with', visibleColumns.length, 'columns');
- headerRow.innerHTML = visibleColumns.map(col => `
- ${prettyLabel(col.name)} |
- `).join('');
+ headerRow.innerHTML = visibleColumns.map(col => {
+ const colSettings = getColumnSettings(col.name);
+ const alignment = colSettings?.alignment || 'left';
+ const wrapClass = colSettings?.headerWrap ? 'whitespace-normal break-words' : 'whitespace-nowrap';
+ return `${prettyLabel(col.name)} | `;
+ }).join('');
}
// Apply global settings to table (styles)
@@ -487,7 +522,32 @@
// Apply row height
if (appSettings && appSettings.rowHeight) {
- styleContent += `.data-table tbody tr { height: ${appSettings.rowHeight}px; }\n`;
+ const height = appSettings.rowHeight;
+ const cellVerticalAlign = appSettings.cellVerticalAlign || 'middle';
+ styleContent += `
+ .data-table tbody tr {
+ height: ${height}px !important;
+ max-height: ${height}px !important;
+ }
+ .data-table tbody td {
+ height: ${height}px !important;
+ max-height: ${height}px !important;
+ overflow: hidden !important;
+ vertical-align: ${cellVerticalAlign} !important;
+ padding: ${cellVerticalAlign === 'top' ? '4px 16px' : (cellVerticalAlign === 'bottom' ? '0 16px 4px 16px' : '0 16px')} !important;
+ }
+ `;
+ }
+
+ // Apply header vertical alignment
+ if (appSettings && appSettings.headerVerticalAlign) {
+ const headerAlign = appSettings.headerVerticalAlign;
+ styleContent += `
+ .data-table thead th {
+ vertical-align: ${headerAlign} !important;
+ padding: ${headerAlign === 'top' ? '8px 16px 4px 16px' : (headerAlign === 'bottom' ? '4px 16px 8px 16px' : '12px 16px')} !important;
+ }
+ `;
}
// Apply font size
@@ -500,6 +560,31 @@
styleContent += `.data-table tbody tr:nth-child(even) td { background: white !important; }\n`;
}
+ // Apply border settings
+ if (appSettings) {
+ const borderColor = appSettings.borderColor || '#e5e7eb';
+ const borderWidth = appSettings.borderWidth || 1;
+ const showRowBorders = appSettings.showRowBorders !== false;
+ const showColumnBorders = appSettings.showColumnBorders !== false;
+
+ styleContent += `
+ .data-table th,
+ .data-table td {
+ border-right: ${showColumnBorders ? `${borderWidth}px solid ${borderColor}` : 'none'} !important;
+ border-bottom: ${showRowBorders ? `${borderWidth}px solid ${borderColor}` : 'none'} !important;
+ }
+ `;
+
+ // Keep header borders slightly different if they exist
+ if (showColumnBorders) {
+ styleContent += `
+ .data-table thead th {
+ border-right: ${borderWidth}px solid rgba(255,255,255,0.2) !important;
+ }
+ `;
+ }
+ }
+
// Column widths and sticky headers based on visible order
if (visibleColumns.length) {
const col1Width = visibleColumns[0]?.width || 120;
@@ -526,6 +611,9 @@
let totalWidth = 0;
visibleColumns.forEach((col, idx) => {
const width = col.width || 150;
+ const colSettings = getColumnSettings(col.name);
+ const alignment = colSettings?.alignment || 'left';
+ const headerWrap = colSettings?.headerWrap;
totalWidth += width;
styleContent += `
.data-table th:nth-child(${idx + 1}),
@@ -533,8 +621,26 @@
width: ${width}px !important;
min-width: ${width}px !important;
max-width: ${width}px !important;
+ text-align: ${alignment};
}
`;
+
+ // Apply header wrapping if enabled
+ if (headerWrap) {
+ styleContent += `
+ .data-table thead th:nth-child(${idx + 1}) {
+ white-space: normal !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ }
+ `;
+ } else {
+ styleContent += `
+ .data-table thead th:nth-child(${idx + 1}) {
+ white-space: nowrap !important;
+ }
+ `;
+ }
});
styleContent += `
@@ -855,6 +961,8 @@
return formatEditableCheckbox(record.Flag, 'Flag', record.id);
case 'Has_Attachments':
return formatEditableCheckbox(record.Has_Attachments, 'Has_Attachments', record.id);
+ case 'Job_Folder_Link':
+ return value ? `${value}` : '';
case 'Notes':
return record.Notes || '';
case 'PSwift_Uploaded':
@@ -880,6 +988,16 @@
// Title attributes for tooltips on truncated cells
function getCellTitle(columnName, record) {
+ // Check if column has hover tooltip enabled
+ const colSettings = getColumnSettings(columnName);
+ if (colSettings && colSettings.showFullTextOnHover) {
+ const value = record[columnName];
+ if (value !== null && value !== undefined) {
+ return String(value);
+ }
+ }
+
+ // Default behavior for specific columns
switch (columnName) {
case 'Notes':
return record.Notes || '';
@@ -919,6 +1037,14 @@
}
allRecords = result.data; // Store all records for filtering
+
+ // Sort by Job_Number descending (highest to lowest) by default
+ allRecords.sort((a, b) => {
+ const numA = parseInt(a.Job_Number) || 0;
+ const numB = parseInt(b.Job_Number) || 0;
+ return numB - numA; // Descending order
+ });
+
console.log(`Loaded ${allRecords.length} jobs ${result.cached ? 'from cache' : 'from PocketBase'}`);
console.log('First record:', allRecords[0]);
@@ -970,7 +1096,7 @@
return;
}
- filteredRecords.forEach(record => {
+ filteredRecords.forEach((record, recordIndex) => {
const row = document.createElement('tr');
row.className = 'border-b border-gray-200';
@@ -983,6 +1109,8 @@
const className = columnClassMap[columnName] || 'px-4 py-3';
let style = '';
const colSettings = getColumnSettings(columnName);
+
+ // Apply choice-based styling
if (colSettings && value && colSettings.choices && colSettings.choices.length > 0) {
const choice = colSettings.choices.find(c => c.value === value);
if (choice) {
@@ -995,14 +1123,56 @@
}
}
}
+ // Apply boolean-based styling for checkbox fields
+ else if (colSettings && (value === true || value === false)) {
+ const bgColor = rowIndex % 2 === 0
+ ? (value ? (colSettings.trueBgColor || '#d1fae5') : (colSettings.falseBgColor || '#fee2e2'))
+ : (value ? (colSettings.trueBgColorAlt || '#a7f3d0') : (colSettings.falseBgColorAlt || '#fecaca'));
+ style += `background-color: ${bgColor} !important;`;
+ }
+
const styleAttr = style ? `style="${style}"` : '';
const titleAttr = titleValue ? ` title="${titleValue}"` : '';
const content = renderCellContent(columnName, record, value);
- return `${content} | `;
+
+ // Make cell editable if appropriate
+ const isEditable = isFieldEditable(columnName, colSettings);
+
+ // For date fields, normalize the value to YYYY-MM-DD format for the date input
+ let dataValue = (value || '').toString().replace(/"/g, '"');
+ if (isEditable && ['Due_Date', 'Start_Date', 'Submission_Date'].includes(columnName) && value) {
+ // Convert ISO date to YYYY-MM-DD
+ try {
+ const dateObj = new Date(value);
+ if (!isNaN(dateObj.getTime())) {
+ const year = dateObj.getFullYear();
+ const month = String(dateObj.getMonth() + 1).padStart(2, '0');
+ const day = String(dateObj.getDate()).padStart(2, '0');
+ dataValue = `${year}-${month}-${day}`;
+ }
+ } catch (e) {
+ // Keep original value if parsing fails
+ }
+ }
+
+ const editableAttr = isEditable ? ` data-editable="true" data-record-id="${record.id}" data-field="${columnName}" data-value="${dataValue}" style="cursor: pointer; ${style}"` : styleAttr;
+
+ return `${content} | `;
}).join('');
row.innerHTML = cellsHtml;
tableBody.appendChild(row);
+
+ // Focus first Job_Number cell on initial load (first row only)
+ if (recordIndex === 0 && currentSearchTerm === '') {
+ setTimeout(() => {
+ const firstJobNumberCell = tableBody.querySelector('tr:first-child td:first-child');
+ if (firstJobNumberCell) {
+ firstJobNumberCell.setAttribute('tabindex', '0');
+ firstJobNumberCell.focus();
+ }
+ }, 100);
+ }
});
}
@@ -1036,6 +1206,324 @@
});
}
+ // Determine if a field is editable
+ function isFieldEditable(columnName, colSettings) {
+ // Checkboxes are already handled by existing checkbox handler
+ const checkboxFields = ['Active', 'Added_To_Calendar', 'Docs_Uploaded', 'Estimate_Approved',
+ 'Estimate_Draft', 'Estimate_Reviewed', 'Flag', 'Has_Attachments',
+ 'PSwift_Uploaded', 'QB_Created', 'Tax_Exempt', 'Tax_Exempt_Docs_Uploaded',
+ 'Voxer_Created'];
+ if (checkboxFields.includes(columnName)) return false;
+
+ // Don't allow editing calculated/display fields
+ const nonEditableFields = ['Job_Number', 'Job_Full_Name', 'Active', 'Due_Date_Counter',
+ 'last_note_at', 'note_count', 'latest_note_summary', 'note_thread_text'];
+ if (nonEditableFields.includes(columnName)) return false;
+
+ // Allow editing for fields with choices or text fields
+ const editableFields = ['Job_Type', 'Job_Status', 'Job_Division', 'Estimator', 'Office_Rep',
+ 'Project_Manager', 'Due_Date', 'Start_Date', 'Submission_Date',
+ 'Notes', 'Job_Name', 'Job_Address', 'Company_Client', 'Contact_Person',
+ 'Phone_Number', 'Due_Date_Notes', 'Start_Date_Notes'];
+
+ return editableFields.includes(columnName);
+ }
+
+ // Normalize date input to YYYY-MM-DD format
+ function normalizeDate(input) {
+ if (!input || input.trim() === '') return null;
+
+ try {
+ // Try parsing the input
+ const date = new Date(input);
+ if (isNaN(date.getTime())) return null;
+
+ // Format as YYYY-MM-DD
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+
+ return `${year}-${month}-${day}`;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ // Create inline confirmation dialog
+ function createConfirmDialog(message, onConfirm, onCancel, anchorElement) {
+ const dialog = document.createElement('div');
+ dialog.className = 'absolute bg-white border-2 border-blue-500 rounded shadow-lg p-3 z-[2000]';
+ dialog.style.minWidth = '200px';
+
+ const messageDiv = document.createElement('div');
+ messageDiv.className = 'text-sm mb-3 text-gray-800';
+ messageDiv.textContent = message;
+
+ const buttonContainer = document.createElement('div');
+ buttonContainer.className = 'flex gap-2 justify-end';
+
+ const confirmBtn = document.createElement('button');
+ confirmBtn.textContent = 'Confirm';
+ confirmBtn.className = 'px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm';
+ confirmBtn.onclick = () => {
+ dialog.remove();
+ onConfirm();
+ };
+
+ const cancelBtn = document.createElement('button');
+ cancelBtn.textContent = 'Cancel';
+ cancelBtn.className = 'px-3 py-1 bg-gray-300 text-gray-700 rounded hover:bg-gray-400 text-sm';
+ cancelBtn.onclick = () => {
+ dialog.remove();
+ onCancel();
+ };
+
+ buttonContainer.appendChild(cancelBtn);
+ buttonContainer.appendChild(confirmBtn);
+
+ dialog.appendChild(messageDiv);
+ dialog.appendChild(buttonContainer);
+
+ // Position near the anchor element
+ document.body.appendChild(dialog);
+ const rect = anchorElement.getBoundingClientRect();
+ dialog.style.left = rect.left + 'px';
+ dialog.style.top = (rect.bottom + 5) + 'px';
+
+ // Focus confirm button
+ confirmBtn.focus();
+
+ return dialog;
+ }
+
+ // Handle cell click for inline editing
+ document.addEventListener('click', async (e) => {
+ const cell = e.target.closest('td[data-editable="true"]');
+ if (!cell) return;
+
+ // Don't trigger if clicking on a checkbox
+ if (e.target.type === 'checkbox') return;
+
+ const fieldName = cell.dataset.field;
+ const recordId = cell.dataset.recordId;
+ const currentValue = cell.dataset.value;
+ const colSettings = getColumnSettings(fieldName);
+
+ // Handle fields with choice dropdowns
+ if (colSettings && colSettings.choices && colSettings.choices.length > 0) {
+ await handleDropdownEdit(cell, fieldName, recordId, currentValue, colSettings);
+ }
+ // Handle date fields
+ else if (['Due_Date', 'Start_Date', 'Submission_Date'].includes(fieldName)) {
+ await handleDateEdit(cell, fieldName, recordId, currentValue);
+ }
+ // Handle text fields
+ else {
+ await handleTextEdit(cell, fieldName, recordId, currentValue);
+ }
+ });
+
+ // Handle dropdown field editing
+ async function handleDropdownEdit(cell, fieldName, recordId, currentValue, colSettings) {
+ const choices = colSettings.choices.map(c => c.value);
+
+ // Create expanded list of choices
+ const choiceContainer = document.createElement('div');
+ choiceContainer.className = 'absolute bg-white border border-gray-300 rounded shadow-lg z-[1000] max-h-64 overflow-y-auto';
+ choiceContainer.style.minWidth = '150px';
+
+ // Add empty option
+ const emptyOption = document.createElement('div');
+ emptyOption.className = 'px-3 py-2 hover:bg-blue-100 cursor-pointer text-sm ' + (currentValue === '' || !currentValue ? 'bg-blue-50 font-semibold' : '');
+ emptyOption.textContent = '(None)';
+ emptyOption.dataset.value = '';
+ choiceContainer.appendChild(emptyOption);
+
+ // Add all choices
+ choices.forEach(choice => {
+ const option = document.createElement('div');
+ option.className = 'px-3 py-2 hover:bg-blue-100 cursor-pointer text-sm ' + (choice === currentValue ? 'bg-blue-50 font-semibold' : '');
+ option.textContent = choice;
+ option.dataset.value = choice;
+ choiceContainer.appendChild(option);
+ });
+
+ // Position near the cell
+ document.body.appendChild(choiceContainer);
+ const rect = cell.getBoundingClientRect();
+ choiceContainer.style.left = rect.left + 'px';
+ choiceContainer.style.top = (rect.bottom + 2) + 'px';
+
+ // Handle choice selection
+ const handleSelection = (newValue) => {
+ choiceContainer.remove();
+
+ if (newValue !== currentValue) {
+ createConfirmDialog(
+ `Change ${fieldName} from "${currentValue || '(empty)'}" to "${newValue || '(empty)'}"?`,
+ async () => {
+ await updateField(recordId, fieldName, newValue || null);
+ },
+ () => {
+ // Cancelled - do nothing
+ },
+ cell
+ );
+ }
+ };
+
+ // Add click handlers to all options
+ choiceContainer.querySelectorAll('div').forEach(option => {
+ option.addEventListener('click', () => {
+ handleSelection(option.dataset.value);
+ });
+ });
+
+ // Close on outside click
+ const closeOnOutsideClick = (e) => {
+ if (!choiceContainer.contains(e.target) && !cell.contains(e.target)) {
+ choiceContainer.remove();
+ document.removeEventListener('click', closeOnOutsideClick);
+ }
+ };
+
+ setTimeout(() => {
+ document.addEventListener('click', closeOnOutsideClick);
+ }, 0);
+
+ // Close on escape
+ const handleEscape = (e) => {
+ if (e.key === 'Escape') {
+ choiceContainer.remove();
+ document.removeEventListener('keydown', handleEscape);
+ }
+ };
+ document.addEventListener('keydown', handleEscape);
+ }
+
+ // Handle date field editing
+ async function handleDateEdit(cell, fieldName, recordId, currentValue) {
+ const input = document.createElement('input');
+ input.type = 'date';
+ input.value = currentValue || '';
+ input.className = 'w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:border-gray-400';
+ input.style.position = 'relative';
+ input.style.zIndex = '1000';
+
+ const originalHTML = cell.innerHTML;
+ const originalPosition = cell.style.position;
+ cell.style.position = 'relative';
+ cell.innerHTML = '';
+ cell.appendChild(input);
+ input.focus();
+ input.showPicker?.(); // Show date picker if supported
+
+ const finishEdit = async () => {
+ const newValue = input.value.trim();
+ cell.innerHTML = originalHTML;
+ cell.style.position = originalPosition;
+
+ if (newValue !== currentValue) {
+ const displayDate = newValue ? new Date(newValue + 'T00:00:00').toLocaleDateString() : '(empty)';
+
+ createConfirmDialog(
+ `Change ${fieldName} to ${displayDate}?`,
+ async () => {
+ await updateField(recordId, fieldName, newValue || null);
+ },
+ () => {
+ // Cancelled - do nothing
+ },
+ cell
+ );
+ }
+ };
+
+ input.addEventListener('blur', finishEdit);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ finishEdit();
+ } else if (e.key === 'Escape') {
+ cell.innerHTML = originalHTML;
+ cell.style.position = originalPosition;
+ }
+ });
+ }
+
+ // Handle text field editing
+ async function handleTextEdit(cell, fieldName, recordId, currentValue) {
+ const isMultiline = fieldName === 'Notes';
+ const input = document.createElement(isMultiline ? 'textarea' : 'input');
+
+ if (!isMultiline) {
+ input.type = 'text';
+ } else {
+ input.rows = 3;
+ }
+
+ input.value = currentValue || '';
+ input.className = 'w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:border-gray-400';
+ input.style.position = 'relative';
+ input.style.zIndex = '1000';
+
+ const originalHTML = cell.innerHTML;
+ const originalPosition = cell.style.position;
+ cell.style.position = 'relative';
+ cell.innerHTML = '';
+ cell.appendChild(input);
+ input.focus();
+ input.select();
+
+ const finishEdit = async () => {
+ const newValue = input.value.trim();
+ cell.innerHTML = originalHTML;
+ cell.style.position = originalPosition;
+
+ if (newValue !== currentValue) {
+ const preview = newValue.length > 50 ? newValue.substring(0, 50) + '...' : newValue;
+
+ createConfirmDialog(
+ `Change ${fieldName} to "${preview || '(empty)'}"?`,
+ async () => {
+ await updateField(recordId, fieldName, newValue || null);
+ },
+ () => {
+ // Cancelled - do nothing
+ },
+ cell
+ );
+ }
+ };
+
+ input.addEventListener('blur', finishEdit);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !isMultiline) {
+ e.preventDefault();
+ finishEdit();
+ } else if (e.key === 'Escape') {
+ cell.innerHTML = originalHTML;
+ cell.style.position = originalPosition;
+ }
+ });
+ }
+
+ // Update field in PocketBase
+ async function updateField(recordId, fieldName, value) {
+ try {
+ await pb.collection('Job_Info_TestEnv').update(recordId, {
+ [fieldName]: value
+ });
+
+ // Reload table data to reflect changes
+ await loadTableData();
+
+ } catch (error) {
+ console.error('Error updating field:', error);
+ alert('Failed to update field. Please try again.');
+ }
+ }
+
// Setup search input listener
function setupSearchListener() {
const searchInput = document.getElementById('searchInput');
@@ -1068,24 +1556,58 @@
if (isReadOnly && fieldName === 'Active') {
// Revert the checkbox state
checkbox.checked = !checkbox.checked;
- alert('Active status is calculated automatically by selected Job Status values.');
+
+ createConfirmDialog(
+ 'Active status is calculated automatically by selected Job Status values.',
+ () => {},
+ () => {},
+ checkbox.closest('td')
+ );
return;
}
// Update PocketBase for editable fields
const newValue = checkbox.checked;
+ const action = newValue ? 'check' : 'uncheck';
- try {
- await pb.collection('Job_Info_TestEnv').update(recordId, {
- [fieldName]: newValue
- });
- // Note: Cache will auto-expire in 5 minutes, no need to clear immediately
- } catch (error) {
- console.error('Error updating field:', error);
- // Revert checkbox on error
- checkbox.checked = !checkbox.checked;
- alert('Failed to update field. Please try again.');
- }
+ // Create confirmation dialog
+ createConfirmDialog(
+ `${action.charAt(0).toUpperCase() + action.slice(1)} ${fieldName}?`,
+ async () => {
+ // Confirmed - update field
+ try {
+ await pb.collection('Job_Info_TestEnv').update(recordId, {
+ [fieldName]: newValue
+ });
+
+ // Special reminder for Tax Exempt
+ if (fieldName === 'Tax_Exempt' && newValue) {
+ setTimeout(() => {
+ createConfirmDialog(
+ 'Reminder: Please upload tax exempt documents.',
+ () => {},
+ () => {},
+ checkbox.closest('td')
+ );
+ }, 100);
+ }
+ } catch (error) {
+ console.error('Error updating field:', error);
+ checkbox.checked = !checkbox.checked;
+ createConfirmDialog(
+ 'Failed to update field. Please try again.',
+ () => {},
+ () => {},
+ checkbox.closest('td')
+ );
+ }
+ },
+ () => {
+ // Cancelled - revert checkbox
+ checkbox.checked = !checkbox.checked;
+ },
+ checkbox.closest('td')
+ );
}
});