Update to v1.0.0-alpha5

- Add hover tooltip column setting for truncated text
- Update Tax_Exempt_Docs_Uploaded label to 'Tax Exempt Uploads?'
This commit is contained in:
2026-01-02 04:18:42 +00:00
parent 5a34170454
commit ad4a4fdf4b
2 changed files with 693 additions and 23 deletions
+150 -2
View File
@@ -82,6 +82,7 @@
<label class="block text-sm font-medium text-gray-700 mb-2">Row Height (px)</label>
<input type="number" id="rowHeight" min="30" max="100"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
<p class="text-xs text-gray-500 mt-1">Fixed height for all data rows</p>
</div>
<div>
@@ -91,11 +92,71 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Striped Rows</label>
<label class="block text-sm font-medium text-gray-700 mb-2">Row Options</label>
<label class="flex items-center space-x-2 mt-2">
<input type="checkbox" id="stripedRows" class="w-5 h-5 text-primary rounded">
<span class="text-sm text-gray-700">Enable alternating row colors</span>
</label>
<label class="flex items-center space-x-2 mt-2">
<input type="checkbox" id="fixedRowHeight" class="w-5 h-5 text-primary rounded">
<span class="text-sm text-gray-700">Fixed row height (no flex)</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Data Cell Vertical Alignment</label>
<select id="cellVerticalAlign"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="top">Top</option>
<option value="middle">Middle</option>
<option value="bottom">Bottom</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Header Vertical Alignment</label>
<select id="headerVerticalAlign"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="top">Top</option>
<option value="middle">Middle</option>
<option value="bottom">Bottom</option>
</select>
</div>
</div>
<div class="mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3">Border Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Border Color</label>
<div class="flex items-center space-x-2">
<input type="color" id="borderColor"
class="w-16 h-10 rounded cursor-pointer border border-gray-300">
<input type="text" id="borderColorText"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary text-sm"
placeholder="#e5e7eb">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Border Width (px)</label>
<input type="number" id="borderWidth" min="0" max="5"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Border Options</label>
<label class="flex items-center space-x-2 mt-2">
<input type="checkbox" id="showRowBorders" class="w-5 h-5 text-primary rounded">
<span class="text-sm text-gray-700">Show row borders</span>
</label>
<label class="flex items-center space-x-2 mt-2">
<input type="checkbox" id="showColumnBorders" class="w-5 h-5 text-primary rounded">
<span class="text-sm text-gray-700">Show column borders</span>
</label>
</div>
</div>
</div>
</div>
@@ -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 @@
</div>
<div>
<label class="flex items-center space-x-2 mt-6">
<label class="block text-sm font-medium text-gray-700 mb-1">Text Alignment</label>
<select data-col-index="${index}" data-field="alignment"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-primary text-sm">
<option value="left" ${(col.alignment || 'left') === 'left' ? 'selected' : ''}>Left</option>
<option value="center" ${col.alignment === 'center' ? 'selected' : ''}>Center</option>
<option value="right" ${col.alignment === 'right' ? 'selected' : ''}>Right</option>
</select>
</div>
<div class="flex flex-col justify-center space-y-1">
<label class="flex items-center space-x-2">
<input type="checkbox" ${col.readOnly ? 'checked' : ''} data-col-index="${index}" data-field="readOnly" class="w-4 h-4">
<span class="text-sm text-gray-700">Read-only</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" ${col.headerWrap ? 'checked' : ''} data-col-index="${index}" data-field="headerWrap" class="w-4 h-4">
<span class="text-sm text-gray-700">Wrap header</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" ${col.showFullTextOnHover ? 'checked' : ''} data-col-index="${index}" data-field="showFullTextOnHover" class="w-4 h-4">
<span class="text-sm text-gray-700">Hover tooltip</span>
</label>
</div>
</div>
<div class="mt-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Boolean Value Formatting (for checkboxes)</label>
</div>
<div class="grid grid-cols-2 gap-4 mb-3">
<div class="border border-gray-200 rounded p-3">
<label class="text-xs font-medium text-gray-600 block mb-2">True Value Style</label>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="text-xs text-gray-600 block mb-1">Background</label>
<input type="color" value="${col.trueBgColor || '#d1fae5'}"
data-col-index="${index}" data-field="trueBgColor"
class="w-full h-8 rounded cursor-pointer">
</div>
<div>
<label class="text-xs text-gray-600 block mb-1">Alt Background</label>
<input type="color" value="${col.trueBgColorAlt || '#a7f3d0'}"
data-col-index="${index}" data-field="trueBgColorAlt"
class="w-full h-8 rounded cursor-pointer">
</div>
</div>
</div>
<div class="border border-gray-200 rounded p-3">
<label class="text-xs font-medium text-gray-600 block mb-2">False Value Style</label>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="text-xs text-gray-600 block mb-1">Background</label>
<input type="color" value="${col.falseBgColor || '#fee2e2'}"
data-col-index="${index}" data-field="falseBgColor"
class="w-full h-8 rounded cursor-pointer">
</div>
<div>
<label class="text-xs text-gray-600 block mb-1">Alt Background</label>
<input type="color" value="${col.falseBgColorAlt || '#fecaca'}"
data-col-index="${index}" data-field="falseBgColorAlt"
class="w-full h-8 rounded cursor-pointer">
</div>
</div>
</div>
</div>
</div>
@@ -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) {
+543 -21
View File
@@ -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 @@
<!-- Footer -->
<div class="mt-2 flex justify-between items-center text-white text-xs px-4">
<span id="footerEmail"></span>
<span>v1.0.0-alpha4</span>
<span>v1.0.0-alpha5</span>
</div>
</div>
@@ -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 => `
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">${prettyLabel(col.name)}</th>
`).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 `<th class="px-4 py-3 text-${alignment} font-semibold ${wrapClass}">${prettyLabel(col.name)}</th>`;
}).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 ? `<a href="${value}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 underline">${value}</a>` : '';
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 `<td class="${className}" ${styleAttr}${titleAttr}>${content}</td>`;
// 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, '&quot;');
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 `<td class="${className}" ${editableAttr}${titleAttr}>${content}</td>`;
}).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')
);
}
});