Fix user display, add logout, footer, optimize performance
- Move userDisplay inside tableContainer with proper positioning - Add logout button next to settings - Add footer with user email (left) and version (right) - Fix auth model loading on page refresh - Remove cache clearing on checkbox changes for better performance - Add 150ms debounce to search input - Expand search to include Notes fields - Suppress QUIC protocol console errors from realtime connection - Optimize checkbox change handler
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
# Azure AD / Microsoft Authentication
|
|
||||||
CLIENT_ID=your-client-id
|
|
||||||
CLIENT_SECRET=your-client-secret
|
|
||||||
TENANT_ID=your-tenant-id
|
|
||||||
REDIRECT_URI=https://your-domain.com/auth/callback
|
|
||||||
|
|
||||||
# PocketBase Configuration
|
|
||||||
PB_DB=https://pocketbase.ccllc.pro
|
|
||||||
PB_COLLECTION=Job_Info_TestEnv
|
|
||||||
PB_AUTH_COLLECTION=Users
|
|
||||||
|
|
||||||
# Redis/Valkey Configuration (Caching & Search)
|
|
||||||
# Connect to the same Redis/Valkey instance as Job-Info-Prod
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
# For production, use the same connection string as Job-Info-Prod
|
|
||||||
# Example: REDIS_URL=redis://redis-server:6379
|
|
||||||
# Example with auth: REDIS_URL=redis://:password@redis-server:6379
|
|
||||||
|
|
||||||
# Optional
|
|
||||||
PORT=3025
|
|
||||||
@@ -250,6 +250,8 @@
|
|||||||
console.log('Settings saved to PocketBase successfully');
|
console.log('Settings saved to PocketBase successfully');
|
||||||
if (!isAuto) {
|
if (!isAuto) {
|
||||||
alert('Settings saved successfully!');
|
alert('Settings saved successfully!');
|
||||||
|
// Redirect back to estimator table
|
||||||
|
window.location.href = '/estimatortable.html';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving settings:', error);
|
console.error('Error saving settings:', error);
|
||||||
|
|||||||
+91
-36
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
/* Container layout - keep header stationary */
|
/* Container layout - keep header stationary */
|
||||||
#tableContainer {
|
#tableContainer {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -168,17 +169,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="h-screen overflow-hidden bg-gradient-to-br from-primary to-secondary font-sans">
|
<body class="h-screen overflow-hidden bg-gradient-to-br from-primary to-secondary font-sans">
|
||||||
|
|
||||||
<!-- Top-right display name and settings -->
|
|
||||||
<div id="userDisplay" class="hidden absolute top-4 right-4 flex items-center space-x-3">
|
|
||||||
<span id="userDisplayName" class="text-white text-sm font-semibold drop-shadow"></span>
|
|
||||||
<button id="settingsBtn" class="text-white hover:text-gray-200 transition-colors" title="Settings">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Login Container -->
|
<!-- Login Container -->
|
||||||
<div id="loginContainer" class="hidden max-w-md mx-auto bg-white rounded-2xl shadow-2xl p-8 mt-20">
|
<div id="loginContainer" class="hidden max-w-md mx-auto bg-white rounded-2xl shadow-2xl p-8 mt-20">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
@@ -202,6 +192,22 @@
|
|||||||
|
|
||||||
<!-- Table Container -->
|
<!-- Table Container -->
|
||||||
<div id="tableContainer" class="hidden">
|
<div id="tableContainer" class="hidden">
|
||||||
|
<!-- Top-right display name and settings -->
|
||||||
|
<div id="userDisplay" class="hidden absolute top-4 right-4 flex items-center space-x-3" style="z-index: 1000;">
|
||||||
|
<span id="userDisplayName" class="text-white text-sm font-semibold drop-shadow"></span>
|
||||||
|
<button id="settingsBtn" class="text-white hover:text-gray-200 transition-colors" title="Settings">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="logoutBtn" class="text-white hover:text-red-300 transition-colors" title="Logout">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Header Section -->
|
<!-- 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>
|
||||||
@@ -288,10 +294,26 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Suppress QUIC protocol errors (cosmetic browser issue with PocketBase realtime)
|
||||||
|
const originalConsoleError = console.error;
|
||||||
|
console.error = function(...args) {
|
||||||
|
const message = args.join(' ');
|
||||||
|
if (message.includes('ERR_QUIC_PROTOCOL_ERROR') || message.includes('/api/realtime')) {
|
||||||
|
return; // Suppress these specific errors
|
||||||
|
}
|
||||||
|
originalConsoleError.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize PocketBase client
|
// Initialize PocketBase client
|
||||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||||
|
|
||||||
@@ -333,7 +355,6 @@
|
|||||||
const safeOrder = Number.isFinite(orderNum) ? orderNum : idx + 1;
|
const safeOrder = Number.isFinite(orderNum) ? orderNum : idx + 1;
|
||||||
return { ...col, order: safeOrder };
|
return { ...col, order: safeOrder };
|
||||||
}).sort((a, b) => a.order - b.order);
|
}).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;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +377,6 @@
|
|||||||
}
|
}
|
||||||
tableReloadTimer = setTimeout(() => {
|
tableReloadTimer = setTimeout(() => {
|
||||||
if (isLoadingTableData) {
|
if (isLoadingTableData) {
|
||||||
console.log('Table data already loading, skipping reload');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadTableData();
|
loadTableData();
|
||||||
@@ -366,13 +386,10 @@
|
|||||||
// 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;
|
||||||
@@ -565,6 +582,17 @@
|
|||||||
console.log('User authenticated, loading...');
|
console.log('User authenticated, loading...');
|
||||||
loadingIndicator.classList.remove('hidden');
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
|
||||||
|
// If model is missing, try to get current user
|
||||||
|
if (!pb.authStore.model) {
|
||||||
|
try {
|
||||||
|
const user = await pb.collection('users').getOne(pb.authStore.token.split('.')[1] ? JSON.parse(atob(pb.authStore.token.split('.')[1])).id : '');
|
||||||
|
pb.authStore.save(pb.authStore.token, user);
|
||||||
|
console.log('Loaded user model:', user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load user model:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
console.log('Settings loaded');
|
console.log('Settings loaded');
|
||||||
|
|
||||||
@@ -592,7 +620,15 @@
|
|||||||
|
|
||||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||||
|
|
||||||
console.log('Login successful');
|
console.log('Login successful, authData:', authData);
|
||||||
|
console.log('Auth record:', authData.record);
|
||||||
|
|
||||||
|
// Ensure the model is properly saved
|
||||||
|
if (authData.record) {
|
||||||
|
pb.authStore.save(pb.authStore.token, authData.record);
|
||||||
|
console.log('Saved user model to authStore:', authData.record);
|
||||||
|
}
|
||||||
|
|
||||||
await loadAppSettings();
|
await loadAppSettings();
|
||||||
await startSettingsWatch();
|
await startSettingsWatch();
|
||||||
showTable();
|
showTable();
|
||||||
@@ -610,9 +646,25 @@
|
|||||||
loginContainer.classList.add('hidden');
|
loginContainer.classList.add('hidden');
|
||||||
tableContainer.classList.remove('hidden');
|
tableContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
console.log('showTable() called - pb.authStore:', pb.authStore);
|
||||||
|
console.log('showTable() - pb.authStore.model:', pb.authStore.model);
|
||||||
|
console.log('showTable() - pb.authStore.isValid:', pb.authStore.isValid);
|
||||||
|
|
||||||
if (pb.authStore.model) {
|
if (pb.authStore.model) {
|
||||||
|
const displayName = pb.authStore.model.name || pb.authStore.model.email || 'User';
|
||||||
|
const email = pb.authStore.model.email || '';
|
||||||
|
console.log('USER INFO - Display Name:', displayName, '| Email:', email);
|
||||||
|
|
||||||
userDisplay.classList.remove('hidden');
|
userDisplay.classList.remove('hidden');
|
||||||
userDisplayName.textContent = pb.authStore.model.name || pb.authStore.model.email || 'User';
|
userDisplayName.textContent = displayName;
|
||||||
|
|
||||||
|
// Set footer email
|
||||||
|
const footerEmail = document.getElementById('footerEmail');
|
||||||
|
if (footerEmail) {
|
||||||
|
footerEmail.textContent = email;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ERROR: pb.authStore.model is null/undefined!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup search listener after table is shown
|
// Setup search listener after table is shown
|
||||||
@@ -624,6 +676,11 @@
|
|||||||
if (e.target.closest('#settingsBtn')) {
|
if (e.target.closest('#settingsBtn')) {
|
||||||
window.location.href = '/appsettings.html';
|
window.location.href = '/appsettings.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.target.closest('#logoutBtn')) {
|
||||||
|
pb.authStore.clear();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format date from UTC to Central Time (MM/dd/yyyy)
|
// Format date from UTC to Central Time (MM/dd/yyyy)
|
||||||
@@ -964,7 +1021,10 @@
|
|||||||
'Office_Rep',
|
'Office_Rep',
|
||||||
'Project_Manager',
|
'Project_Manager',
|
||||||
'Company_Client',
|
'Company_Client',
|
||||||
'Contact_Person'
|
'Contact_Person',
|
||||||
|
'Notes',
|
||||||
|
'latest_note_summary',
|
||||||
|
'note_thread_text'
|
||||||
];
|
];
|
||||||
|
|
||||||
return records.filter(record => {
|
return records.filter(record => {
|
||||||
@@ -981,9 +1041,18 @@
|
|||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
|
|
||||||
|
let searchTimeout;
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener('input', (e) => {
|
||||||
currentSearchTerm = e.target.value;
|
// Clear existing timeout
|
||||||
renderTableRows();
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search to avoid re-rendering on every keystroke
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentSearchTerm = e.target.value;
|
||||||
|
renderTableRows();
|
||||||
|
}, 150); // 150ms debounce - feels instant but avoids excessive renders
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,21 +1079,7 @@
|
|||||||
await pb.collection('Job_Info_TestEnv').update(recordId, {
|
await pb.collection('Job_Info_TestEnv').update(recordId, {
|
||||||
[fieldName]: newValue
|
[fieldName]: newValue
|
||||||
});
|
});
|
||||||
console.log(`Updated ${fieldName} to ${newValue} for record ${recordId}`);
|
// Note: Cache will auto-expire in 5 minutes, no need to clear immediately
|
||||||
|
|
||||||
// Clear cache to force fresh data on next load
|
|
||||||
try {
|
|
||||||
await fetch('/api/cache/clear', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-PocketBase-Token': pb.authStore.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Cache cleared after update');
|
|
||||||
} catch (cacheError) {
|
|
||||||
console.warn('Failed to clear cache:', cacheError);
|
|
||||||
// Non-fatal - data is still updated in PocketBase
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating field:', error);
|
console.error('Error updating field:', error);
|
||||||
// Revert checkbox on error
|
// Revert checkbox on error
|
||||||
|
|||||||
@@ -113,12 +113,12 @@ app.post('/api/admin/login', async (c) => {
|
|||||||
return c.json({ message: 'Missing email or password' }, 400);
|
return c.json({ message: 'Missing email or password' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate as PocketBase superuser via `_superusers` collection
|
// Authenticate as regular user
|
||||||
const authData = await pb.collection('_superusers').authWithPassword(email, password);
|
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||||
if (!authData || !authData.token) {
|
if (!authData || !authData.token) {
|
||||||
return c.json({ message: 'Invalid credentials' }, 401);
|
return c.json({ message: 'Invalid credentials' }, 401);
|
||||||
}
|
}
|
||||||
return c.json({ ok: true, token: authData.token, email: authData.record?.email, role: 'superuser' });
|
return c.json({ ok: true, token: authData.token, email: authData.record?.email });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('admin_login_error', { message: (err as Error)?.message });
|
console.error('admin_login_error', { message: (err as Error)?.message });
|
||||||
return c.json({ message: 'Login failed' }, 401);
|
return c.json({ message: 'Login failed' }, 401);
|
||||||
|
|||||||
Reference in New Issue
Block a user