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');
|
||||
if (!isAuto) {
|
||||
alert('Settings saved successfully!');
|
||||
// Redirect back to estimator table
|
||||
window.location.href = '/estimatortable.html';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
|
||||
+91
-36
@@ -26,6 +26,7 @@
|
||||
|
||||
/* Container layout - keep header stationary */
|
||||
#tableContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
@@ -168,17 +169,6 @@
|
||||
</head>
|
||||
<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 -->
|
||||
<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">
|
||||
@@ -202,6 +192,22 @@
|
||||
|
||||
<!-- Table Container -->
|
||||
<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 -->
|
||||
<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>
|
||||
@@ -288,10 +294,26 @@
|
||||
</table>
|
||||
</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>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></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
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
@@ -333,7 +355,6 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -356,7 +377,6 @@
|
||||
}
|
||||
tableReloadTimer = setTimeout(() => {
|
||||
if (isLoadingTableData) {
|
||||
console.log('Table data already loading, skipping reload');
|
||||
return;
|
||||
}
|
||||
loadTableData();
|
||||
@@ -366,13 +386,10 @@
|
||||
// Load app settings from PocketBase (global settings)
|
||||
async function loadAppSettings() {
|
||||
try {
|
||||
console.log('Loading app settings for:', APP_NAME);
|
||||
const records = await pb.collection('app_preferences_settings').getFullList({
|
||||
filter: `app_name = "${APP_NAME}"`
|
||||
});
|
||||
|
||||
console.log('Settings records found:', records.length);
|
||||
|
||||
if (records.length > 0) {
|
||||
const settingsData = records[0].app_json_prefs;
|
||||
appSettingsRecordId = records[0].id;
|
||||
@@ -565,6 +582,17 @@
|
||||
console.log('User authenticated, loading...');
|
||||
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();
|
||||
console.log('Settings loaded');
|
||||
|
||||
@@ -592,7 +620,15 @@
|
||||
|
||||
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 startSettingsWatch();
|
||||
showTable();
|
||||
@@ -610,9 +646,25 @@
|
||||
loginContainer.classList.add('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) {
|
||||
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');
|
||||
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
|
||||
@@ -624,6 +676,11 @@
|
||||
if (e.target.closest('#settingsBtn')) {
|
||||
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)
|
||||
@@ -964,7 +1021,10 @@
|
||||
'Office_Rep',
|
||||
'Project_Manager',
|
||||
'Company_Client',
|
||||
'Contact_Person'
|
||||
'Contact_Person',
|
||||
'Notes',
|
||||
'latest_note_summary',
|
||||
'note_thread_text'
|
||||
];
|
||||
|
||||
return records.filter(record => {
|
||||
@@ -981,9 +1041,18 @@
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (!searchInput) return;
|
||||
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
currentSearchTerm = e.target.value;
|
||||
renderTableRows();
|
||||
// Clear existing timeout
|
||||
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, {
|
||||
[fieldName]: newValue
|
||||
});
|
||||
console.log(`Updated ${fieldName} to ${newValue} for record ${recordId}`);
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -113,12 +113,12 @@ app.post('/api/admin/login', async (c) => {
|
||||
return c.json({ message: 'Missing email or password' }, 400);
|
||||
}
|
||||
|
||||
// Authenticate as PocketBase superuser via `_superusers` collection
|
||||
const authData = await pb.collection('_superusers').authWithPassword(email, password);
|
||||
// Authenticate as regular user
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
if (!authData || !authData.token) {
|
||||
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) {
|
||||
console.error('admin_login_error', { message: (err as Error)?.message });
|
||||
return c.json({ message: 'Login failed' }, 401);
|
||||
|
||||
Reference in New Issue
Block a user