Fix syntax error and reduce search section spacing
This commit is contained in:
+176
-43
@@ -202,12 +202,34 @@
|
|||||||
|
|
||||||
<!-- Table Container -->
|
<!-- Table Container -->
|
||||||
<div id="tableContainer" class="hidden">
|
<div id="tableContainer" class="hidden">
|
||||||
|
<!-- Header Section -->
|
||||||
<div class="header-section bg-white rounded-lg shadow-2xl p-6">
|
<div class="header-section bg-white rounded-lg shadow-2xl p-6">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Estimator Job Table</h1>
|
<h1 class="text-3xl font-bold text-gray-800">Estimator Job Table</h1>
|
||||||
<p class="text-gray-600 mt-1">Scroll horizontally and vertically • First two columns are frozen</p>
|
<p class="text-gray-600 mt-1">Scroll horizontally and vertically • First two columns are frozen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-2xl p-4" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
|
<!-- Search Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-2xl px-4 py-2 mt-2">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div style="width: 400px;">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="searchInput"
|
||||||
|
placeholder="Search jobs..."
|
||||||
|
class="w-full px-4 py-1.5 pl-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<svg class="absolute left-3 top-2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span id="searchResultCount" class="absolute right-3 top-2 text-sm text-gray-600"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-2xl p-4 mt-2" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table w-full text-sm">
|
<table class="data-table w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -301,6 +323,8 @@
|
|||||||
let appSettingsUnsubscribe = null;
|
let appSettingsUnsubscribe = null;
|
||||||
let tableReloadTimer = null;
|
let tableReloadTimer = null;
|
||||||
let isLoadingTableData = false;
|
let isLoadingTableData = false;
|
||||||
|
let allRecords = []; // Store all records for filtering
|
||||||
|
let currentSearchTerm = '';
|
||||||
|
|
||||||
function normalizeColumnOrders(columns) {
|
function normalizeColumnOrders(columns) {
|
||||||
if (!Array.isArray(columns)) return [];
|
if (!Array.isArray(columns)) return [];
|
||||||
@@ -342,13 +366,17 @@
|
|||||||
// Load app settings from PocketBase (global settings)
|
// Load app settings from PocketBase (global settings)
|
||||||
async function loadAppSettings() {
|
async function loadAppSettings() {
|
||||||
try {
|
try {
|
||||||
|
console.log('Loading app settings for:', APP_NAME);
|
||||||
const records = await pb.collection('app_preferences_settings').getFullList({
|
const records = await pb.collection('app_preferences_settings').getFullList({
|
||||||
filter: `app_name = "${APP_NAME}"`
|
filter: `app_name = "${APP_NAME}"`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Settings records found:', records.length);
|
||||||
|
|
||||||
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;
|
appSettingsRecordId = records[0].id;
|
||||||
|
console.log('Settings record ID:', appSettingsRecordId);
|
||||||
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
|
// Ensure all columns have order field
|
||||||
@@ -366,11 +394,16 @@
|
|||||||
console.log('Loaded app settings:', appSettings);
|
console.log('Loaded app settings:', appSettings);
|
||||||
applyGlobalSettings();
|
applyGlobalSettings();
|
||||||
renderTableHeader();
|
renderTableHeader();
|
||||||
scheduleTableReload();
|
} else {
|
||||||
|
console.log('No settings data in record');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No settings record found, using defaults');
|
||||||
|
renderTableHeader();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading app settings:', error);
|
console.error('Error loading app settings:', error);
|
||||||
|
renderTableHeader(); // Still render with defaults
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +448,12 @@
|
|||||||
// Render table header dynamically based on current settings
|
// Render table header dynamically based on current settings
|
||||||
function renderTableHeader() {
|
function renderTableHeader() {
|
||||||
const headerRow = document.querySelector('.data-table thead tr');
|
const headerRow = document.querySelector('.data-table thead tr');
|
||||||
|
if (!headerRow) {
|
||||||
|
console.error('Header row not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const visibleColumns = getVisibleColumns();
|
const visibleColumns = getVisibleColumns();
|
||||||
|
console.log('Rendering header with', visibleColumns.length, 'columns');
|
||||||
headerRow.innerHTML = visibleColumns.map(col => `
|
headerRow.innerHTML = visibleColumns.map(col => `
|
||||||
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">${prettyLabel(col.name)}</th>
|
<th class="px-4 py-3 text-left font-semibold whitespace-nowrap">${prettyLabel(col.name)}</th>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -498,8 +536,11 @@
|
|||||||
// Ensure header reflects current order/visibility
|
// Ensure header reflects current order/visibility
|
||||||
renderTableHeader();
|
renderTableHeader();
|
||||||
|
|
||||||
// Re-render body to reflect new order/visibility
|
// Re-render body only if data is already loaded
|
||||||
scheduleTableReload();
|
if (allRecords && allRecords.length > 0) {
|
||||||
|
renderTableRows();
|
||||||
|
}
|
||||||
|
// Note: Don't call scheduleTableReload here - it will be called after initial load
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get column settings
|
// Get column settings
|
||||||
@@ -521,12 +562,23 @@
|
|||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
if (pb.authStore.isValid) {
|
if (pb.authStore.isValid) {
|
||||||
// Already logged in
|
// Already logged in
|
||||||
|
console.log('User authenticated, loading...');
|
||||||
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
|
console.log('Settings loaded');
|
||||||
|
|
||||||
await startSettingsWatch();
|
await startSettingsWatch();
|
||||||
|
console.log('Settings watch started');
|
||||||
|
|
||||||
showTable();
|
showTable();
|
||||||
|
console.log('Table shown');
|
||||||
|
|
||||||
await loadTableData();
|
await loadTableData();
|
||||||
|
console.log('Data loaded');
|
||||||
} else {
|
} else {
|
||||||
// Need to login
|
// Need to login
|
||||||
|
console.log('User not authenticated');
|
||||||
loginContainer.classList.remove('hidden');
|
loginContainer.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,9 +588,11 @@
|
|||||||
try {
|
try {
|
||||||
loginBtn.disabled = true;
|
loginBtn.disabled = true;
|
||||||
loginBtn.textContent = 'Logging in...';
|
loginBtn.textContent = 'Logging in...';
|
||||||
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
|
||||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||||
|
|
||||||
|
console.log('Login successful');
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
await startSettingsWatch();
|
await startSettingsWatch();
|
||||||
showTable();
|
showTable();
|
||||||
@@ -548,6 +602,7 @@
|
|||||||
alert('Login failed. Please try again.');
|
alert('Login failed. Please try again.');
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = 'Login with Microsoft';
|
loginBtn.textContent = 'Login with Microsoft';
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -559,6 +614,9 @@
|
|||||||
userDisplay.classList.remove('hidden');
|
userDisplay.classList.remove('hidden');
|
||||||
userDisplayName.textContent = pb.authStore.model.name || pb.authStore.model.email || 'User';
|
userDisplayName.textContent = pb.authStore.model.name || pb.authStore.model.email || 'User';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup search listener after table is shown
|
||||||
|
setupSearchListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings button handler
|
// Settings button handler
|
||||||
@@ -803,46 +861,13 @@
|
|||||||
throw new Error(result.message || 'Failed to fetch jobs');
|
throw new Error(result.message || 'Failed to fetch jobs');
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = result.data;
|
allRecords = result.data; // Store all records for filtering
|
||||||
console.log(`Loaded ${records.length} jobs ${result.cached ? 'from cache' : 'from PocketBase'}`);
|
console.log(`Loaded ${allRecords.length} jobs ${result.cached ? 'from cache' : 'from PocketBase'}`);
|
||||||
|
console.log('First record:', allRecords[0]);
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
// Render with current search filter
|
||||||
const visibleColumns = getVisibleColumns();
|
renderTableRows();
|
||||||
|
console.log('Render complete');
|
||||||
records.forEach(record => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.className = 'border-b border-gray-200';
|
|
||||||
|
|
||||||
const rowIndex = Array.from(tableBody.children).length;
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
const colSettings = getColumnSettings(columnName);
|
|
||||||
if (colSettings && value && colSettings.choices && colSettings.choices.length > 0) {
|
|
||||||
const choice = colSettings.choices.find(c => c.value === value);
|
|
||||||
if (choice) {
|
|
||||||
const bgColor = rowIndex % 2 === 0 ? choice.bgColor : choice.bgColorAlt;
|
|
||||||
style += `background-color: ${bgColor} !important;`;
|
|
||||||
if (choice.fontStyle === 'bold') {
|
|
||||||
style += 'font-weight: bold;';
|
|
||||||
} else if (choice.fontStyle === 'italic') {
|
|
||||||
style += 'font-style: italic;';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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('');
|
|
||||||
|
|
||||||
row.innerHTML = cellsHtml;
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
isLoadingTableData = false;
|
isLoadingTableData = false;
|
||||||
loadingIndicator.classList.add('hidden');
|
loadingIndicator.classList.add('hidden');
|
||||||
@@ -854,6 +879,114 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render table rows with optional filtering
|
||||||
|
function renderTableRows() {
|
||||||
|
if (!tableBody) {
|
||||||
|
console.error('tableBody element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allRecords || allRecords.length === 0) {
|
||||||
|
console.log('No records to render');
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="50" class="px-4 py-8 text-center text-gray-500">No data available</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
const visibleColumns = getVisibleColumns();
|
||||||
|
|
||||||
|
// Filter records based on search term
|
||||||
|
const filteredRecords = filterRecords(allRecords, currentSearchTerm);
|
||||||
|
|
||||||
|
// Update search result count
|
||||||
|
const searchResultCount = document.getElementById('searchResultCount');
|
||||||
|
if (searchResultCount) {
|
||||||
|
if (currentSearchTerm.trim()) {
|
||||||
|
searchResultCount.textContent = `${filteredRecords.length} of ${allRecords.length}`;
|
||||||
|
} else {
|
||||||
|
searchResultCount.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredRecords.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="50" class="px-4 py-8 text-center text-gray-500">No matching results</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredRecords.forEach(record => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'border-b border-gray-200';
|
||||||
|
|
||||||
|
const rowIndex = Array.from(tableBody.children).length;
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
const colSettings = getColumnSettings(columnName);
|
||||||
|
if (colSettings && value && colSettings.choices && colSettings.choices.length > 0) {
|
||||||
|
const choice = colSettings.choices.find(c => c.value === value);
|
||||||
|
if (choice) {
|
||||||
|
const bgColor = rowIndex % 2 === 0 ? choice.bgColor : choice.bgColorAlt;
|
||||||
|
style += `background-color: ${bgColor} !important;`;
|
||||||
|
if (choice.fontStyle === 'bold') {
|
||||||
|
style += 'font-weight: bold;';
|
||||||
|
} else if (choice.fontStyle === 'italic') {
|
||||||
|
style += 'font-style: italic;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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('');
|
||||||
|
|
||||||
|
row.innerHTML = cellsHtml;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter records based on search term
|
||||||
|
function filterRecords(records, searchTerm) {
|
||||||
|
if (!searchTerm || !searchTerm.trim()) {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
const term = searchTerm.toLowerCase().trim();
|
||||||
|
const searchFields = [
|
||||||
|
'Job_Number',
|
||||||
|
'Job_Full_Name',
|
||||||
|
'Job_Address',
|
||||||
|
'Estimator',
|
||||||
|
'Office_Rep',
|
||||||
|
'Project_Manager',
|
||||||
|
'Company_Client',
|
||||||
|
'Contact_Person'
|
||||||
|
];
|
||||||
|
|
||||||
|
return records.filter(record => {
|
||||||
|
return searchFields.some(field => {
|
||||||
|
const value = record[field];
|
||||||
|
if (!value) return false;
|
||||||
|
return String(value).toLowerCase().includes(term);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup search input listener
|
||||||
|
function setupSearchListener() {
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
if (!searchInput) return;
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
currentSearchTerm = e.target.value;
|
||||||
|
renderTableRows();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle checkbox changes
|
// Handle checkbox changes
|
||||||
document.addEventListener('change', async (e) => {
|
document.addEventListener('change', async (e) => {
|
||||||
if (e.target.type === 'checkbox' && e.target.dataset.field) {
|
if (e.target.type === 'checkbox' && e.target.dataset.field) {
|
||||||
|
|||||||
Reference in New Issue
Block a user