Implement dynamic column reordering with numeric order field and realtime sync
- Add order field to all columns in DEFAULT_SETTINGS (1-47) - Settings page: reorder columns via numeric input or up/down arrows, autosave after 400ms - Estimator table: render columns in order specified by order field, refresh on settings updates - Normalize column orders on load and realtime updates with fallback for missing order - Add logging to trace column order changes through settings and estimator pages - Reset button reinitializes all settings to defaults with proper column ordering - Table reload prevents concurrent requests to avoid PocketBase auto-cancellation
This commit is contained in:
+145
-40
@@ -133,6 +133,8 @@
|
|||||||
const APP_NAME = 'Estimator Job List';
|
const APP_NAME = 'Estimator Job List';
|
||||||
let settingsRecordId = null;
|
let settingsRecordId = null;
|
||||||
let currentSettings = null;
|
let currentSettings = null;
|
||||||
|
let autosaveTimer = null;
|
||||||
|
const AUTOSAVE_DELAY = 400;
|
||||||
|
|
||||||
// Column names from schema
|
// Column names from schema
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
@@ -153,8 +155,9 @@
|
|||||||
rowHeight: 40,
|
rowHeight: 40,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
stripedRows: true,
|
stripedRows: true,
|
||||||
columns: COLUMNS.map(col => ({
|
columns: COLUMNS.map((col, idx) => ({
|
||||||
name: col,
|
name: col,
|
||||||
|
order: idx + 1,
|
||||||
readOnly: col === 'Active' || col === 'Due_Date_Counter' || col === 'Job_Number',
|
readOnly: col === 'Active' || col === 'Due_Date_Counter' || col === 'Job_Number',
|
||||||
width: col === 'Job_Number' ? 120 : col === 'Job_Full_Name' ? 250 : 150,
|
width: col === 'Job_Number' ? 120 : col === 'Job_Full_Name' ? 250 : 150,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -170,6 +173,79 @@
|
|||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function syncColumnOrderNumbers() {
|
||||||
|
if (!currentSettings || !Array.isArray(currentSettings.columns)) return;
|
||||||
|
currentSettings.columns = currentSettings.columns.map((col, idx) => ({
|
||||||
|
...col,
|
||||||
|
order: idx + 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reinitializeToDefaults() {
|
||||||
|
try {
|
||||||
|
// Create fresh settings from defaults
|
||||||
|
currentSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
console.log('Reinitialized to defaults, saving...');
|
||||||
|
|
||||||
|
// Immediately save to PocketBase (not queued)
|
||||||
|
const jsonString = JSON.stringify(currentSettings);
|
||||||
|
await pb.collection('app_preferences_settings').update(settingsRecordId, {
|
||||||
|
app_json_prefs: jsonString,
|
||||||
|
app_txt_prefs: `Reset to defaults on ${new Date().toISOString()}`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Settings reset to defaults and saved');
|
||||||
|
renderSettings();
|
||||||
|
alert('Settings reset to defaults successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reinitializing settings:', error);
|
||||||
|
alert('Error resetting to defaults: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncColumnOrderNumbers() {
|
||||||
|
if (!currentSettings || !Array.isArray(currentSettings.columns)) return;
|
||||||
|
currentSettings.columns = currentSettings.columns.map((col, idx) => ({
|
||||||
|
...col,
|
||||||
|
order: idx + 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureGlobalInputs() {
|
||||||
|
currentSettings.rowHeight = parseInt(document.getElementById('rowHeight').value);
|
||||||
|
currentSettings.fontSize = parseInt(document.getElementById('fontSize').value);
|
||||||
|
currentSettings.stripedRows = document.getElementById('stripedRows').checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings(isAuto = false) {
|
||||||
|
try {
|
||||||
|
captureGlobalInputs();
|
||||||
|
syncColumnOrderNumbers();
|
||||||
|
console.log('Saving columns with orders:', currentSettings.columns.map(c => ({ name: c.name, order: c.order, visible: c.visible })).slice(0, 5));
|
||||||
|
const jsonString = JSON.stringify(currentSettings);
|
||||||
|
await pb.collection('app_preferences_settings').update(settingsRecordId, {
|
||||||
|
app_json_prefs: jsonString,
|
||||||
|
app_txt_prefs: `Updated on ${new Date().toISOString()}`
|
||||||
|
});
|
||||||
|
console.log('Settings saved to PocketBase successfully');
|
||||||
|
if (!isAuto) {
|
||||||
|
alert('Settings saved successfully!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving settings:', error);
|
||||||
|
if (!isAuto) {
|
||||||
|
alert('Error saving settings: ' + error.message + '. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueAutosave() {
|
||||||
|
if (autosaveTimer) {
|
||||||
|
clearTimeout(autosaveTimer);
|
||||||
|
}
|
||||||
|
autosaveTimer = setTimeout(() => saveSettings(true), AUTOSAVE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
// Load settings from PocketBase
|
// Load settings from PocketBase
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -219,6 +295,7 @@
|
|||||||
console.log('Created new record:', settingsRecordId);
|
console.log('Created new record:', settingsRecordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncColumnOrderNumbers();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||||
document.getElementById('settingsContainer').classList.remove('hidden');
|
document.getElementById('settingsContainer').classList.remove('hidden');
|
||||||
@@ -227,6 +304,7 @@
|
|||||||
console.error('Error details:', error.message, error.stack);
|
console.error('Error details:', error.message, error.stack);
|
||||||
alert('Error loading settings: ' + error.message + '. Using defaults.');
|
alert('Error loading settings: ' + error.message + '. Using defaults.');
|
||||||
currentSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
currentSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
syncColumnOrderNumbers();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||||
document.getElementById('settingsContainer').classList.remove('hidden');
|
document.getElementById('settingsContainer').classList.remove('hidden');
|
||||||
@@ -250,13 +328,19 @@
|
|||||||
colDiv.innerHTML = `
|
colDiv.innerHTML = `
|
||||||
<div class="flex justify-between items-center mb-3">
|
<div class="flex justify-between items-center mb-3">
|
||||||
<h3 class="text-lg font-semibold text-gray-800">${col.name}</h3>
|
<h3 class="text-lg font-semibold text-gray-800">${col.name}</h3>
|
||||||
<label class="flex items-center space-x-2">
|
<div class="flex items-center space-x-3">
|
||||||
<input type="checkbox" ${col.visible ? 'checked' : ''} data-col-index="${index}" data-field="visible" class="w-4 h-4">
|
<div class="flex flex-col text-xs text-gray-500">
|
||||||
<span class="text-sm text-gray-700">Visible</span>
|
<button class="hover:text-primary" data-col-index="${index}" data-action="move-up" title="Move up">▲</button>
|
||||||
</label>
|
<button class="hover:text-primary" data-col-index="${index}" data-action="move-down" title="Move down">▼</button>
|
||||||
|
</div>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" ${col.visible ? 'checked' : ''} data-col-index="${index}" data-field="visible" class="w-4 h-4">
|
||||||
|
<span class="text-sm text-gray-700">Visible</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-3">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Column Width (px)</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Column Width (px)</label>
|
||||||
<input type="number" value="${col.width}" min="50" max="500"
|
<input type="number" value="${col.width}" min="50" max="500"
|
||||||
@@ -264,6 +348,13 @@
|
|||||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-primary text-sm">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-primary text-sm">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Order</label>
|
||||||
|
<input type="number" value="${col.order ?? index + 1}" min="1" max="${currentSettings.columns.length}"
|
||||||
|
data-col-index="${index}" data-action="set-order"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-primary text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="flex items-center space-x-2 mt-6">
|
<label class="flex items-center space-x-2 mt-6">
|
||||||
<input type="checkbox" ${col.readOnly ? 'checked' : ''} data-col-index="${index}" data-field="readOnly" class="w-4 h-4">
|
<input type="checkbox" ${col.readOnly ? 'checked' : ''} data-col-index="${index}" data-field="readOnly" class="w-4 h-4">
|
||||||
@@ -379,6 +470,9 @@
|
|||||||
} else {
|
} else {
|
||||||
currentSettings.columns[colIndex][field] = field === 'width' ? parseInt(value) : value;
|
currentSettings.columns[colIndex][field] = field === 'width' ? parseInt(value) : value;
|
||||||
console.log('Updated column field:', field, currentSettings.columns[colIndex][field]);
|
console.log('Updated column field:', field, currentSettings.columns[colIndex][field]);
|
||||||
|
if (field === 'visible' || field === 'readOnly' || field === 'width') {
|
||||||
|
queueAutosave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -388,15 +482,44 @@
|
|||||||
el.addEventListener('input', handler);
|
el.addEventListener('input', handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add/Remove choice buttons
|
// Add/Remove choice buttons and column move / set-order
|
||||||
document.querySelectorAll('[data-action]').forEach(btn => {
|
document.querySelectorAll('[data-action]').forEach(el => {
|
||||||
const handler = (e) => {
|
const handler = (e) => {
|
||||||
const action = e.target.dataset.action;
|
const action = e.target.dataset.action;
|
||||||
const colIndex = parseInt(e.target.dataset.colIndex);
|
const colIndex = parseInt(e.target.dataset.colIndex);
|
||||||
|
|
||||||
console.log('Action:', action, 'colIndex:', colIndex);
|
console.log('Action:', action, 'colIndex:', colIndex);
|
||||||
|
|
||||||
if (action === 'add-choice') {
|
if (action === 'set-order') {
|
||||||
|
const desired = Math.max(1, Math.min(currentSettings.columns.length, parseInt(e.target.value || '0', 10)));
|
||||||
|
const targetIndex = desired - 1;
|
||||||
|
if (targetIndex === colIndex) return;
|
||||||
|
const [col] = currentSettings.columns.splice(colIndex, 1);
|
||||||
|
currentSettings.columns.splice(targetIndex, 0, col);
|
||||||
|
console.log('Moved column', col.name, 'from index', colIndex, 'to', targetIndex);
|
||||||
|
syncColumnOrderNumbers();
|
||||||
|
console.log('Column orders after sync:', currentSettings.columns.map(c => ({ name: c.name, order: c.order })).slice(0, 5));
|
||||||
|
renderSettings();
|
||||||
|
queueAutosave();
|
||||||
|
} else if (action === 'move-up') {
|
||||||
|
if (colIndex > 0) {
|
||||||
|
const tmp = currentSettings.columns[colIndex - 1];
|
||||||
|
currentSettings.columns[colIndex - 1] = currentSettings.columns[colIndex];
|
||||||
|
currentSettings.columns[colIndex] = tmp;
|
||||||
|
syncColumnOrderNumbers();
|
||||||
|
renderSettings();
|
||||||
|
queueAutosave();
|
||||||
|
}
|
||||||
|
} else if (action === 'move-down') {
|
||||||
|
if (colIndex < currentSettings.columns.length - 1) {
|
||||||
|
const tmp = currentSettings.columns[colIndex + 1];
|
||||||
|
currentSettings.columns[colIndex + 1] = currentSettings.columns[colIndex];
|
||||||
|
currentSettings.columns[colIndex] = tmp;
|
||||||
|
syncColumnOrderNumbers();
|
||||||
|
renderSettings();
|
||||||
|
queueAutosave();
|
||||||
|
}
|
||||||
|
} else if (action === 'add-choice') {
|
||||||
if (!currentSettings.columns[colIndex].choices) {
|
if (!currentSettings.columns[colIndex].choices) {
|
||||||
currentSettings.columns[colIndex].choices = [];
|
currentSettings.columns[colIndex].choices = [];
|
||||||
}
|
}
|
||||||
@@ -408,54 +531,36 @@
|
|||||||
});
|
});
|
||||||
console.log('Added choice, total:', currentSettings.columns[colIndex].choices.length);
|
console.log('Added choice, total:', currentSettings.columns[colIndex].choices.length);
|
||||||
renderSettings();
|
renderSettings();
|
||||||
|
queueAutosave();
|
||||||
} else if (action === 'remove-choice') {
|
} else if (action === 'remove-choice') {
|
||||||
const choiceIndex = parseInt(e.target.dataset.choiceIndex);
|
const choiceIndex = parseInt(e.target.dataset.choiceIndex);
|
||||||
currentSettings.columns[colIndex].choices.splice(choiceIndex, 1);
|
currentSettings.columns[colIndex].choices.splice(choiceIndex, 1);
|
||||||
console.log('Removed choice at index:', choiceIndex);
|
console.log('Removed choice at index:', choiceIndex);
|
||||||
renderSettings();
|
renderSettings();
|
||||||
|
queueAutosave();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
btn.removeEventListener('click', handler);
|
// Use click for buttons; change (not input) for numeric order to avoid immediate rerender while typing
|
||||||
btn.addEventListener('click', handler);
|
el.removeEventListener('click', handler);
|
||||||
|
el.removeEventListener('change', handler);
|
||||||
|
if (el.tagName === 'INPUT' && el.type === 'number' && el.dataset.action === 'set-order') {
|
||||||
|
el.addEventListener('change', handler);
|
||||||
|
} else {
|
||||||
|
el.addEventListener('click', handler);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
document.getElementById('saveBtn').addEventListener('click', async () => {
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||||
try {
|
await saveSettings(false);
|
||||||
// Update global settings
|
|
||||||
currentSettings.rowHeight = parseInt(document.getElementById('rowHeight').value);
|
|
||||||
currentSettings.fontSize = parseInt(document.getElementById('fontSize').value);
|
|
||||||
currentSettings.stripedRows = document.getElementById('stripedRows').checked;
|
|
||||||
|
|
||||||
console.log('Saving settings:', currentSettings);
|
|
||||||
console.log('Settings record ID:', settingsRecordId);
|
|
||||||
|
|
||||||
const jsonString = JSON.stringify(currentSettings);
|
|
||||||
console.log('JSON string length:', jsonString.length);
|
|
||||||
|
|
||||||
// Save to PocketBase
|
|
||||||
const updated = await pb.collection('app_preferences_settings').update(settingsRecordId, {
|
|
||||||
app_json_prefs: jsonString,
|
|
||||||
app_txt_prefs: `Updated on ${new Date().toISOString()}`
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Settings saved successfully:', updated);
|
|
||||||
alert('Settings saved successfully!');
|
|
||||||
window.location.href = '/estimatortable.html';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving settings:', error);
|
|
||||||
console.error('Error details:', error.message, error.stack);
|
|
||||||
alert('Error saving settings: ' + error.message + '. Please try again.');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset to defaults
|
// Reset to defaults
|
||||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||||
if (confirm('Are you sure you want to reset all settings to defaults?')) {
|
if (confirm('Are you sure you want to reset all settings to defaults? This will reset all columns to their original order.')) {
|
||||||
currentSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
reinitializeToDefaults();
|
||||||
renderSettings();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+273
-137
@@ -273,7 +273,7 @@
|
|||||||
// Initialize PocketBase client
|
// Initialize PocketBase client
|
||||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||||
|
|
||||||
// Column name to position mapping (based on actual table order)
|
// Column name to position mapping fallback (when no settings found)
|
||||||
const COLUMN_ORDER = [
|
const COLUMN_ORDER = [
|
||||||
'Job_Number', 'Job_Full_Name', 'Active', 'Added_To_Calendar', 'Attachment_List',
|
'Job_Number', 'Job_Full_Name', 'Active', 'Added_To_Calendar', 'Attachment_List',
|
||||||
'Company_Client', 'Contact_Person', 'Docs_Uploaded', 'Due_Date', 'Due_Date_Counter',
|
'Company_Client', 'Contact_Person', 'Docs_Uploaded', 'Due_Date', 'Due_Date_Counter',
|
||||||
@@ -297,6 +297,47 @@
|
|||||||
|
|
||||||
const APP_NAME = 'Estimator Job List';
|
const APP_NAME = 'Estimator Job List';
|
||||||
let appSettings = null;
|
let appSettings = null;
|
||||||
|
let appSettingsRecordId = null;
|
||||||
|
let appSettingsUnsubscribe = null;
|
||||||
|
let tableReloadTimer = null;
|
||||||
|
let isLoadingTableData = false;
|
||||||
|
|
||||||
|
function normalizeColumnOrders(columns) {
|
||||||
|
if (!Array.isArray(columns)) return [];
|
||||||
|
const normalized = columns.map((col, idx) => {
|
||||||
|
const orderNum = Number.parseInt(col?.order, 10);
|
||||||
|
const safeOrder = Number.isFinite(orderNum) ? orderNum : idx + 1;
|
||||||
|
return { ...col, order: safeOrder };
|
||||||
|
}).sort((a, b) => a.order - b.order);
|
||||||
|
console.log('Normalized columns (first 5):', normalized.slice(0, 5).map(c => ({ name: c.name, order: c.order, visible: c.visible })));
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers to get columns in current order/visibility
|
||||||
|
function getVisibleColumns() {
|
||||||
|
if (appSettings && Array.isArray(appSettings.columns) && appSettings.columns.length) {
|
||||||
|
return normalizeColumnOrders(appSettings.columns).filter((c) => c.visible !== false);
|
||||||
|
}
|
||||||
|
// Fallback to default order when no settings yet
|
||||||
|
return COLUMN_ORDER.map((name, idx) => ({ name, visible: true, order: idx + 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyLabel(name) {
|
||||||
|
return name.replace(/_/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleTableReload() {
|
||||||
|
if (tableReloadTimer) {
|
||||||
|
clearTimeout(tableReloadTimer);
|
||||||
|
}
|
||||||
|
tableReloadTimer = setTimeout(() => {
|
||||||
|
if (isLoadingTableData) {
|
||||||
|
console.log('Table data already loading, skipping reload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadTableData();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
// Load app settings from PocketBase
|
// Load app settings from PocketBase
|
||||||
async function loadAppSettings() {
|
async function loadAppSettings() {
|
||||||
@@ -307,10 +348,25 @@
|
|||||||
|
|
||||||
if (records.length > 0) {
|
if (records.length > 0) {
|
||||||
const settingsData = records[0].app_json_prefs;
|
const settingsData = records[0].app_json_prefs;
|
||||||
|
appSettingsRecordId = records[0].id;
|
||||||
if (settingsData) {
|
if (settingsData) {
|
||||||
appSettings = typeof settingsData === 'string' ? JSON.parse(settingsData) : settingsData;
|
appSettings = typeof settingsData === 'string' ? JSON.parse(settingsData) : settingsData;
|
||||||
|
// Ensure all columns have order field
|
||||||
|
if (appSettings && Array.isArray(appSettings.columns)) {
|
||||||
|
const needsOrderSync = appSettings.columns.some(col => col.order === undefined || col.order === null);
|
||||||
|
if (needsOrderSync) {
|
||||||
|
console.log('Columns missing order field, syncing...');
|
||||||
|
appSettings.columns = appSettings.columns.map((col, idx) => ({
|
||||||
|
...col,
|
||||||
|
order: col.order ?? idx + 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
appSettings.columns = normalizeColumnOrders(appSettings.columns);
|
||||||
|
}
|
||||||
console.log('Loaded app settings:', appSettings);
|
console.log('Loaded app settings:', appSettings);
|
||||||
applyGlobalSettings();
|
applyGlobalSettings();
|
||||||
|
renderTableHeader();
|
||||||
|
scheduleTableReload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -318,98 +374,114 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply global settings to table
|
// Watch for live updates to app settings
|
||||||
function applyGlobalSettings() {
|
async function startSettingsWatch() {
|
||||||
if (!appSettings) return;
|
// Avoid duplicate subscriptions
|
||||||
|
if (appSettingsUnsubscribe) return;
|
||||||
|
try {
|
||||||
|
appSettingsUnsubscribe = await pb.collection('app_preferences_settings').subscribe('*', (e) => {
|
||||||
|
const record = e?.record;
|
||||||
|
if (!record) return;
|
||||||
|
if (record.app_name !== APP_NAME) return;
|
||||||
|
// If we didn't have the record id yet, capture it
|
||||||
|
if (!appSettingsRecordId) appSettingsRecordId = record.id;
|
||||||
|
// Only respond to the tracked settings record if known
|
||||||
|
if (appSettingsRecordId && record.id !== appSettingsRecordId) return;
|
||||||
|
|
||||||
|
const settingsData = record.app_json_prefs;
|
||||||
|
appSettings = typeof settingsData === 'string' ? JSON.parse(settingsData || '{}') : (settingsData || {});
|
||||||
|
if (appSettings && Array.isArray(appSettings.columns)) {
|
||||||
|
const needsOrderSync = appSettings.columns.some(col => col.order === undefined || col.order === null);
|
||||||
|
if (needsOrderSync) {
|
||||||
|
console.log('Realtime update: columns missing order field, syncing...');
|
||||||
|
appSettings.columns = appSettings.columns.map((col, idx) => ({
|
||||||
|
...col,
|
||||||
|
order: col.order ?? idx + 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
console.log('Received settings update via realtime, original orders:', appSettings.columns.map(c => ({ name: c.name, order: c.order })).slice(0, 5));
|
||||||
|
appSettings.columns = normalizeColumnOrders(appSettings.columns);
|
||||||
|
console.log('After normalize/sort:', appSettings.columns.map(c => ({ name: c.name, order: c.order })).slice(0, 5));
|
||||||
|
}
|
||||||
|
console.log('App settings updated via realtime', { action: e.action });
|
||||||
|
applyGlobalSettings();
|
||||||
|
scheduleTableReload();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to start settings watch', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render table header dynamically based on current settings
|
||||||
|
function renderTableHeader() {
|
||||||
|
const headerRow = document.querySelector('.data-table thead tr');
|
||||||
|
const visibleColumns = getVisibleColumns();
|
||||||
|
headerRow.innerHTML = visibleColumns.map(col => `
|
||||||
|
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">${prettyLabel(col.name)}</th>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply global settings to table (styles)
|
||||||
|
function applyGlobalSettings() {
|
||||||
// Remove any existing dynamic styles first
|
// Remove any existing dynamic styles first
|
||||||
const existingStyle = document.getElementById('dynamic-column-styles');
|
const existingStyle = document.getElementById('dynamic-column-styles');
|
||||||
if (existingStyle) {
|
if (existingStyle) existingStyle.remove();
|
||||||
existingStyle.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const visibleColumns = getVisibleColumns();
|
||||||
let styleContent = '';
|
let styleContent = '';
|
||||||
|
|
||||||
// Apply row height
|
// Apply row height
|
||||||
if (appSettings.rowHeight) {
|
if (appSettings && appSettings.rowHeight) {
|
||||||
styleContent += `.data-table tbody tr { height: ${appSettings.rowHeight}px; }\n`;
|
styleContent += `.data-table tbody tr { height: ${appSettings.rowHeight}px; }\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply font size
|
// Apply font size
|
||||||
if (appSettings.fontSize) {
|
if (appSettings && appSettings.fontSize) {
|
||||||
styleContent += `.data-table { font-size: ${appSettings.fontSize}px; }\n`;
|
styleContent += `.data-table { font-size: ${appSettings.fontSize}px; }\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply striped rows setting
|
// Apply striped rows setting
|
||||||
if (appSettings.stripedRows === false) {
|
if (appSettings && appSettings.stripedRows === false) {
|
||||||
styleContent += `.data-table tbody tr:nth-child(even) td { background: white !important; }\n`;
|
styleContent += `.data-table tbody tr:nth-child(even) td { background: white !important; }\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply column widths and visibility dynamically
|
// Column widths and sticky headers based on visible order
|
||||||
if (appSettings.columns) {
|
if (visibleColumns.length) {
|
||||||
const col1Settings = getColumnSettings('Job_Number');
|
const col1Width = visibleColumns[0]?.width || 120;
|
||||||
const col2Settings = getColumnSettings('Job_Full_Name');
|
const col2Width = visibleColumns[1]?.width || 250;
|
||||||
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 += `
|
styleContent += `
|
||||||
.data-table th:nth-child(1), .data-table td:nth-child(1) {
|
.data-table th:nth-child(1), .data-table td:nth-child(1) {
|
||||||
width: ${col1Width}px !important;
|
width: ${col1Width}px !important;
|
||||||
min-width: ${col1Width}px !important;
|
min-width: ${col1Width}px !important;
|
||||||
max-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;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
if (visibleColumns.length > 1) {
|
||||||
|
styleContent += `
|
||||||
|
.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;
|
let totalWidth = 0;
|
||||||
|
visibleColumns.forEach((col, idx) => {
|
||||||
appSettings.columns.forEach((col) => {
|
const width = col.width || 150;
|
||||||
// Find the actual position in the table
|
totalWidth += width;
|
||||||
const colPosition = COLUMN_ORDER.indexOf(col.name) + 1;
|
styleContent += `
|
||||||
|
.data-table th:nth-child(${idx + 1}),
|
||||||
if (colPosition === 0) {
|
.data-table td:nth-child(${idx + 1}) {
|
||||||
console.warn(`Column ${col.name} not found in COLUMN_ORDER`);
|
width: ${width}px !important;
|
||||||
return;
|
min-width: ${width}px !important;
|
||||||
}
|
max-width: ${width}px !important;
|
||||||
|
}
|
||||||
// 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 += `
|
styleContent += `
|
||||||
.data-table {
|
.data-table {
|
||||||
width: ${totalWidth}px !important;
|
width: ${totalWidth}px !important;
|
||||||
@@ -422,6 +494,12 @@
|
|||||||
style.id = 'dynamic-column-styles';
|
style.id = 'dynamic-column-styles';
|
||||||
style.textContent = styleContent;
|
style.textContent = styleContent;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Ensure header reflects current order/visibility
|
||||||
|
renderTableHeader();
|
||||||
|
|
||||||
|
// Re-render body to reflect new order/visibility
|
||||||
|
scheduleTableReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get column settings
|
// Get column settings
|
||||||
@@ -444,6 +522,7 @@
|
|||||||
if (pb.authStore.isValid) {
|
if (pb.authStore.isValid) {
|
||||||
// Already logged in
|
// Already logged in
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
|
await startSettingsWatch();
|
||||||
showTable();
|
showTable();
|
||||||
await loadTableData();
|
await loadTableData();
|
||||||
} else {
|
} else {
|
||||||
@@ -461,6 +540,7 @@
|
|||||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||||
|
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
|
await startSettingsWatch();
|
||||||
showTable();
|
showTable();
|
||||||
await loadTableData();
|
await loadTableData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -610,106 +690,150 @@
|
|||||||
return `<div class="checkbox-cell"><input type="checkbox" ${value ? 'checked' : ''} data-field="${fieldName}" data-record-id="${recordId}"></div>`;
|
return `<div class="checkbox-cell"><input type="checkbox" ${value ? 'checked' : ''} data-field="${fieldName}" data-record-id="${recordId}"></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load table data from PocketBase
|
// Per-column classes to keep layout/styling consistent when order changes
|
||||||
|
const columnClassMap = {
|
||||||
|
Job_Number: 'px-4 py-3 border-r border-gray-200 whitespace-nowrap font-medium',
|
||||||
|
Job_Full_Name: 'px-4 py-3 border-r border-gray-200 whitespace-nowrap',
|
||||||
|
Active: 'px-4 py-3 text-center',
|
||||||
|
Added_To_Calendar: 'px-4 py-3 text-center',
|
||||||
|
Docs_Uploaded: 'px-4 py-3 text-center',
|
||||||
|
Due_Date: 'px-4 py-3 whitespace-nowrap',
|
||||||
|
Estimate_Approved: 'px-4 py-3 text-center',
|
||||||
|
Estimate_Draft: 'px-4 py-3 text-center',
|
||||||
|
Estimate_Reviewed: 'px-4 py-3 text-center',
|
||||||
|
Flag: 'px-4 py-3 text-center',
|
||||||
|
Has_Attachments: 'px-4 py-3 text-center',
|
||||||
|
Notes: 'px-4 py-3 max-w-xs truncate',
|
||||||
|
PSwift_Uploaded: 'px-4 py-3 text-center',
|
||||||
|
QB_Created: 'px-4 py-3 text-center',
|
||||||
|
Start_Date: 'px-4 py-3 whitespace-nowrap',
|
||||||
|
Submission_Date: 'px-4 py-3 whitespace-nowrap',
|
||||||
|
Tax_Exempt: 'px-4 py-3 text-center',
|
||||||
|
Tax_Exempt_Docs_Uploaded: 'px-4 py-3 text-center',
|
||||||
|
Voxer_Created: 'px-4 py-3 text-center',
|
||||||
|
last_note_at: 'px-4 py-3 whitespace-nowrap',
|
||||||
|
latest_note_summary: 'px-4 py-3 max-w-xs truncate',
|
||||||
|
note_count: 'px-4 py-3 text-center',
|
||||||
|
note_thread_text: 'px-4 py-3 max-w-xs truncate'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render cell content for each supported column
|
||||||
|
function renderCellContent(columnName, record, value) {
|
||||||
|
switch (columnName) {
|
||||||
|
case 'Active':
|
||||||
|
return formatActiveCheckbox(record.Active, record.id);
|
||||||
|
case 'Added_To_Calendar':
|
||||||
|
return formatEditableCheckbox(record.Added_To_Calendar, 'Added_To_Calendar', record.id);
|
||||||
|
case 'Docs_Uploaded':
|
||||||
|
return formatEditableCheckbox(record.Docs_Uploaded, 'Docs_Uploaded', record.id);
|
||||||
|
case 'Due_Date':
|
||||||
|
return formatDate(record.Due_Date, record.Due_Date_Notes);
|
||||||
|
case 'Due_Date_Counter':
|
||||||
|
return calculateDueDateCounter(record);
|
||||||
|
case 'Estimate_Approved':
|
||||||
|
return formatEditableCheckbox(record.Estimate_Approved, 'Estimate_Approved', record.id);
|
||||||
|
case 'Estimate_Draft':
|
||||||
|
return formatEditableCheckbox(record.Estimate_Draft, 'Estimate_Draft', record.id);
|
||||||
|
case 'Estimate_Reviewed':
|
||||||
|
return formatEditableCheckbox(record.Estimate_Reviewed, 'Estimate_Reviewed', record.id);
|
||||||
|
case 'Flag':
|
||||||
|
return formatEditableCheckbox(record.Flag, 'Flag', record.id);
|
||||||
|
case 'Has_Attachments':
|
||||||
|
return formatEditableCheckbox(record.Has_Attachments, 'Has_Attachments', record.id);
|
||||||
|
case 'Notes':
|
||||||
|
return record.Notes || '';
|
||||||
|
case 'PSwift_Uploaded':
|
||||||
|
return formatEditableCheckbox(record.PSwift_Uploaded, 'PSwift_Uploaded', record.id);
|
||||||
|
case 'QB_Created':
|
||||||
|
return formatEditableCheckbox(record.QB_Created, 'QB_Created', record.id);
|
||||||
|
case 'Start_Date':
|
||||||
|
return formatDate(record.Start_Date, record.Start_Date_Notes);
|
||||||
|
case 'Submission_Date':
|
||||||
|
return formatDate(record.Submission_Date);
|
||||||
|
case 'Tax_Exempt':
|
||||||
|
return formatEditableCheckbox(record.Tax_Exempt, 'Tax_Exempt', record.id);
|
||||||
|
case 'Tax_Exempt_Docs_Uploaded':
|
||||||
|
return formatEditableCheckbox(record.Tax_Exempt_Docs_Uploaded, 'Tax_Exempt_Docs_Uploaded', record.id);
|
||||||
|
case 'Voxer_Created':
|
||||||
|
return formatEditableCheckbox(record.Voxer_Created, 'Voxer_Created', record.id);
|
||||||
|
case 'last_note_at':
|
||||||
|
return formatDateTime(record.last_note_at);
|
||||||
|
default:
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title attributes for tooltips on truncated cells
|
||||||
|
function getCellTitle(columnName, record) {
|
||||||
|
switch (columnName) {
|
||||||
|
case 'Notes':
|
||||||
|
return record.Notes || '';
|
||||||
|
case 'latest_note_summary':
|
||||||
|
return record.latest_note_summary || '';
|
||||||
|
case 'note_thread_text':
|
||||||
|
return record.note_thread_text || '';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load table data from PocketBase respecting current column order/visibility
|
||||||
async function loadTableData() {
|
async function loadTableData() {
|
||||||
try {
|
try {
|
||||||
|
if (isLoadingTableData) {
|
||||||
|
console.log('Table data already loading, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoadingTableData = true;
|
||||||
loadingIndicator.classList.remove('hidden');
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
|
||||||
// Fetch all records from Job_Info_TestEnv collection
|
|
||||||
const records = await pb.collection('Job_Info_TestEnv').getFullList({
|
const records = await pb.collection('Job_Info_TestEnv').getFullList({
|
||||||
sort: 'Job_Number',
|
sort: 'Job_Number',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear existing rows
|
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
|
const visibleColumns = getVisibleColumns();
|
||||||
|
|
||||||
// Populate table
|
|
||||||
records.forEach(record => {
|
records.forEach(record => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.className = 'border-b border-gray-200';
|
row.className = 'border-b border-gray-200';
|
||||||
|
|
||||||
// Helper to get cell style based on column settings and value
|
const rowIndex = Array.from(tableBody.children).length;
|
||||||
const getCellStyle = (columnName, value) => {
|
|
||||||
|
const cellsHtml = visibleColumns.map((col, idx) => {
|
||||||
|
const columnName = col.name;
|
||||||
|
const value = record[columnName];
|
||||||
|
const titleValue = getCellTitle(columnName, record);
|
||||||
|
const className = columnClassMap[columnName] || 'px-4 py-3';
|
||||||
let style = '';
|
let style = '';
|
||||||
const colSettings = getColumnSettings(columnName);
|
const colSettings = getColumnSettings(columnName);
|
||||||
|
if (colSettings && value && colSettings.choices && colSettings.choices.length > 0) {
|
||||||
if (colSettings) {
|
const choice = colSettings.choices.find(c => c.value === value);
|
||||||
// Check for choice-based styling
|
if (choice) {
|
||||||
if (value && colSettings.choices && colSettings.choices.length > 0) {
|
const bgColor = rowIndex % 2 === 0 ? choice.bgColor : choice.bgColorAlt;
|
||||||
const choice = colSettings.choices.find(c => c.value === value);
|
style += `background-color: ${bgColor} !important;`;
|
||||||
if (choice) {
|
if (choice.fontStyle === 'bold') {
|
||||||
// Use alternating colors based on row index
|
style += 'font-weight: bold;';
|
||||||
const rowIndex = Array.from(tableBody.children).length;
|
} else if (choice.fontStyle === 'italic') {
|
||||||
const bgColor = rowIndex % 2 === 0 ? choice.bgColor : choice.bgColorAlt;
|
style += 'font-style: italic;';
|
||||||
style += `background-color: ${bgColor} !important;`;
|
|
||||||
|
|
||||||
if (choice.fontStyle === 'bold') {
|
|
||||||
style += 'font-weight: bold;';
|
|
||||||
} else if (choice.fontStyle === 'italic') {
|
|
||||||
style += 'font-style: italic;';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const styleAttr = style ? `style="${style}"` : '';
|
||||||
|
const titleAttr = titleValue ? ` title="${titleValue}"` : '';
|
||||||
|
const content = renderCellContent(columnName, record, value);
|
||||||
|
return `<td class="${className}" ${styleAttr}${titleAttr}>${content}</td>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
return style ? `style="${style}"` : '';
|
row.innerHTML = cellsHtml;
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isLoadingTableData = false;
|
||||||
loadingIndicator.classList.add('hidden');
|
loadingIndicator.classList.add('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading table data:', error);
|
console.error('Error loading table data:', error);
|
||||||
|
isLoadingTableData = false;
|
||||||
loadingIndicator.classList.add('hidden');
|
loadingIndicator.classList.add('hidden');
|
||||||
alert('Error loading data. Please refresh the page.');
|
alert('Error loading data. Please refresh the page.');
|
||||||
}
|
}
|
||||||
@@ -750,6 +874,18 @@
|
|||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
|
||||||
|
// Clean up subscription on unload
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
try {
|
||||||
|
if (appSettingsUnsubscribe) {
|
||||||
|
appSettingsUnsubscribe();
|
||||||
|
appSettingsUnsubscribe = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Error while unsubscribing settings watch', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -466,9 +466,9 @@ app.get('/api/export/schema', async (c) => {
|
|||||||
|
|
||||||
const PORT = Number(process.env.PORT || 6500);
|
const PORT = Number(process.env.PORT || 6500);
|
||||||
|
|
||||||
console.log(`\n🚀 Server running on port ${PORT}`);
|
// Keep concise startup pointers
|
||||||
console.log(`📊 Estimator Table: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
|
console.log(`📊 Estimator Table: http://localhost:${PORT}/`);
|
||||||
console.log(`⚙️ Settings: \x1b[36mhttp://localhost:${PORT}/appsettings.html\x1b[0m\n`);
|
console.log(`⚙️ Settings: http://localhost:${PORT}/appsettings.html`);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: PORT,
|
port: PORT,
|
||||||
|
|||||||
Reference in New Issue
Block a user