Manager Info UI improvements and voice search enhancements

- Removed duplicate Links section above notes
- Updated file icons to use SVG badges (w-3 h-3) - PDF red, Word blue, image icons
- Reduced notes viewing area height to 120px with vertical scroll
- Voice search: Clear text when all highlighted and mic clicked
- Voice search: Replace 'new' with 'latest' in dictations
- Voice search: Remove trailing 'job' from dictations
- Voice search: Auto-open Manager Info when exactly 1 result found
This commit is contained in:
2025-12-28 05:55:03 +00:00
parent eead6de627
commit dd0eba8aee
+84 -134
View File
@@ -698,6 +698,12 @@
// Remove trailing punctuation (period, comma, etc.) // Remove trailing punctuation (period, comma, etc.)
transcript = transcript.replace(/[.,!?;:]+$/, ''); transcript = transcript.replace(/[.,!?;:]+$/, '');
// Remove trailing "job" if present
transcript = transcript.replace(/\s+job$/i, '');
// Replace "new" with "latest"
transcript = transcript.replace(/\bnew\b/gi, 'latest');
// Capitalize each word (title case) // Capitalize each word (title case)
transcript = transcript.split(' ').map(word => transcript = transcript.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
@@ -721,6 +727,16 @@
// Trigger search with results // Trigger search with results
applyFiltersAndRender(); applyFiltersAndRender();
// If exactly 1 result, automatically open Manager Info
if (visibleJobs.length === 1) {
const job = visibleJobs[0];
currentJob = job;
detailModal.classList.remove('hidden');
document.getElementById('detailBox').scrollTop = 0;
switchTab('managerInfo');
populateManagerInfo(job);
}
// Check if all results are final, if so prepare to stop // Check if all results are final, if so prepare to stop
const allFinal = Array.from(event.results).every(r => r.isFinal); const allFinal = Array.from(event.results).every(r => r.isFinal);
if (allFinal && event.results.length > 0) { if (allFinal && event.results.length > 0) {
@@ -770,6 +786,12 @@
return; return;
} }
// Check if text is highlighted and clear it if so
if (searchBox.selectionStart === 0 && searchBox.selectionEnd === searchBox.value.length && searchBox.value.length > 0) {
searchBox.value = '';
baseSearchText = '';
}
// Request permission first time // Request permission first time
if (!hasPermission) { if (!hasPermission) {
const granted = await requestMicPermission(); const granted = await requestMicPermission();
@@ -1314,102 +1336,6 @@
row5.appendChild(estimatedEndDate); row5.appendChild(estimatedEndDate);
managerInfoTable.appendChild(row5); managerInfoTable.appendChild(row5);
// Links Section: Plans, Estimates, Documents & Files
if(job.Job_Folder_Link && String(job.Job_Folder_Link).trim() !== ''){
try {
const files = await fetchJobFiles(job);
// Categorize files
const plans = [];
const estimates = [];
const documentsAndFiles = [];
files.forEach(file => {
const name = file.name.toLowerCase();
if(name.includes('plan') || name.includes('drawing') || name.includes('dwg')) {
plans.push(file);
} else if(name.includes('estimate') || name.includes('est')) {
estimates.push(file);
} else {
documentsAndFiles.push(file);
}
});
// Links Section Container
const linksSection = document.createElement('div');
linksSection.className = 'mt-4 pb-3 border-b border-gray-200';
linksSection.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-2" style="font-size: 8px;">Links</div>`;
const linksGrid = document.createElement('div');
linksGrid.className = 'grid grid-cols-3 gap-4';
// Plans column
const plansCol = document.createElement('div');
plansCol.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Plans</div>`;
const plansList = document.createElement('div');
plansList.className = 'space-y-1';
if(plans.length > 0) {
plans.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'text-blue-600 hover:text-blue-800 cursor-pointer underline';
fileItem.style.fontSize = '10px';
fileItem.textContent = file.name;
fileItem.onclick = () => openFilePreview(file);
plansList.appendChild(fileItem);
});
} else {
plansList.innerHTML = `<div class="text-gray-400 italic" style="font-size: 10px;">No plans</div>`;
}
plansCol.appendChild(plansList);
// Estimates column
const estimatesCol = document.createElement('div');
estimatesCol.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Estimates</div>`;
const estimatesList = document.createElement('div');
estimatesList.className = 'space-y-1';
if(estimates.length > 0) {
estimates.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'text-blue-600 hover:text-blue-800 cursor-pointer underline';
fileItem.style.fontSize = '10px';
fileItem.textContent = file.name;
fileItem.onclick = () => openFilePreview(file);
estimatesList.appendChild(fileItem);
});
} else {
estimatesList.innerHTML = `<div class="text-gray-400 italic" style="font-size: 10px;">No estimates</div>`;
}
estimatesCol.appendChild(estimatesList);
// Documents & Files column
const docsCol = document.createElement('div');
docsCol.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Documents & Files</div>`;
const docsList = document.createElement('div');
docsList.className = 'space-y-1';
if(documentsAndFiles.length > 0) {
documentsAndFiles.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'text-blue-600 hover:text-blue-800 cursor-pointer underline';
fileItem.style.fontSize = '10px';
fileItem.textContent = file.name;
fileItem.onclick = () => openFilePreview(file);
docsList.appendChild(fileItem);
});
} else {
docsList.innerHTML = `<div class="text-gray-400 italic" style="font-size: 10px;">No documents</div>`;
}
docsCol.appendChild(docsList);
linksGrid.appendChild(plansCol);
linksGrid.appendChild(estimatesCol);
linksGrid.appendChild(docsCol);
linksSection.appendChild(linksGrid);
managerInfoTable.appendChild(linksSection);
} catch(err) {
console.log('Error loading files for Manager Info:', err);
}
}
// Notes Section with Tabs // Notes Section with Tabs
const notesSection = document.createElement('div'); const notesSection = document.createElement('div');
notesSection.className = 'mt-4'; notesSection.className = 'mt-4';
@@ -1439,12 +1365,21 @@
jobNotesContent.id = 'jobNotesContent'; jobNotesContent.id = 'jobNotesContent';
jobNotesContent.className = 'notes-tab-content'; jobNotesContent.className = 'notes-tab-content';
// Container for display with expand button
const jobNotesDisplayContainer = document.createElement('div');
jobNotesDisplayContainer.className = 'relative mb-3';
// Display notes first // Display notes first
const jobNotesDisplay = document.createElement('div'); const jobNotesDisplay = document.createElement('div');
jobNotesDisplay.className = 'text-gray-600 whitespace-pre-wrap break-words p-2 border border-gray-300 rounded bg-gray-50 min-h-[80px] mb-3'; jobNotesDisplay.id = 'jobNotesDisplay';
jobNotesDisplay.className = 'text-gray-600 whitespace-pre-wrap break-words p-2 border border-gray-300 rounded bg-gray-50 overflow-y-auto';
jobNotesDisplay.style.fontSize = '10px'; jobNotesDisplay.style.fontSize = '10px';
jobNotesDisplay.style.height = '120px';
jobNotesDisplay.style.maxHeight = '120px';
jobNotesDisplay.innerHTML = job.Notes || '<span class="text-gray-400 italic">No notes available</span>'; jobNotesDisplay.innerHTML = job.Notes || '<span class="text-gray-400 italic">No notes available</span>';
jobNotesContent.appendChild(jobNotesDisplay); jobNotesDisplayContainer.appendChild(jobNotesDisplay);
jobNotesContent.appendChild(jobNotesDisplayContainer);
// Add Note button below notes display // Add Note button below notes display
const addNoteBtn = document.createElement('button'); const addNoteBtn = document.createElement('button');
@@ -1509,12 +1444,21 @@
managerNotesContent.id = 'managerNotesContent'; managerNotesContent.id = 'managerNotesContent';
managerNotesContent.className = 'notes-tab-content hidden'; managerNotesContent.className = 'notes-tab-content hidden';
// Container for display with expand button
const managerNotesDisplayContainer = document.createElement('div');
managerNotesDisplayContainer.className = 'relative mb-3';
// Display manager notes first // Display manager notes first
const managerNotesDisplay = document.createElement('div'); const managerNotesDisplay = document.createElement('div');
managerNotesDisplay.className = 'text-gray-600 whitespace-pre-wrap break-words p-2 border border-gray-300 rounded bg-gray-50 min-h-[80px] mb-3'; managerNotesDisplay.id = 'managerNotesDisplay';
managerNotesDisplay.className = 'text-gray-600 whitespace-pre-wrap break-words p-2 border border-gray-300 rounded bg-gray-50 overflow-y-auto';
managerNotesDisplay.style.fontSize = '10px'; managerNotesDisplay.style.fontSize = '10px';
managerNotesDisplay.style.height = '120px';
managerNotesDisplay.style.maxHeight = '120px';
managerNotesDisplay.innerHTML = '<span class="text-gray-400 italic">No manager notes available</span>'; managerNotesDisplay.innerHTML = '<span class="text-gray-400 italic">No manager notes available</span>';
managerNotesContent.appendChild(managerNotesDisplay); managerNotesDisplayContainer.appendChild(managerNotesDisplay);
managerNotesContent.appendChild(managerNotesDisplayContainer);
// Add Note button for manager below notes display // Add Note button for manager below notes display
const addManagerNoteBtn = document.createElement('button'); const addManagerNoteBtn = document.createElement('button');
@@ -1899,7 +1843,7 @@
}); });
const currentUserName = getDisplayName(); const currentUserName = getDisplayName();
const noteType = isManagerNote ? 'manager' : 'job'; const noteType = isManagerNote ? 'manager' : 'job';
const typeLabel = isManagerNote ? '[MANAGER NOTE]' : ''; const typeLabel = isManagerNote ? '[MANAGER]' : '';
const formatted = `${typeLabel}[${currentUserName} - ${timestamp}] ${html}`; const formatted = `${typeLabel}[${currentUserName} - ${timestamp}] ${html}`;
try { try {
@@ -2008,8 +1952,8 @@
const jobNotes = allNotes.filter(n => !n.type || n.type === 'job'); const jobNotes = allNotes.filter(n => !n.type || n.type === 'job');
const managerNotes = allNotes.filter(n => n.type === 'manager'); const managerNotes = allNotes.filter(n => n.type === 'manager');
// Update job notes display // Update job notes display using ID selector
const jobNotesDisplay = document.querySelector('#jobNotesContent > div:first-child'); const jobNotesDisplay = document.getElementById('jobNotesDisplay');
if(jobNotesDisplay) { if(jobNotesDisplay) {
if(jobNotes.length > 0) { if(jobNotes.length > 0) {
jobNotesDisplay.innerHTML = jobNotes.map(n => n.Note).join('\n\n'); jobNotesDisplay.innerHTML = jobNotes.map(n => n.Note).join('\n\n');
@@ -2018,8 +1962,8 @@
} }
} }
// Update manager notes display // Update manager notes display using ID selector
const managerNotesDisplay = document.querySelector('#managerNotesContent > div:first-child'); const managerNotesDisplay = document.getElementById('managerNotesDisplay');
if(managerNotesDisplay) { if(managerNotesDisplay) {
if(managerNotes.length > 0) { if(managerNotes.length > 0) {
managerNotesDisplay.innerHTML = managerNotes.map(n => n.Note).join('\n\n'); managerNotesDisplay.innerHTML = managerNotes.map(n => n.Note).join('\n\n');
@@ -2061,6 +2005,7 @@
otherFiles.push(file); otherFiles.push(file);
} }
}); });
console.log('Categorized - Plans:', plans.length, 'Estimates:', estimates.length, 'Other:', otherFiles.length);
} }
// Plans column // Plans column
@@ -2074,10 +2019,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer'; fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px'; fileItem.style.fontSize = '10px';
// File icon // File icon using getFileIcon
const icon = document.createElement('span'); const iconWrapper = document.createElement('span');
icon.textContent = '📄'; iconWrapper.className = 'flex-shrink-0';
fileItem.appendChild(icon); iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
fileItem.appendChild(iconWrapper);
const fileName = document.createElement('span'); const fileName = document.createElement('span');
fileName.textContent = file.name; fileName.textContent = file.name;
@@ -2103,10 +2049,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer'; fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px'; fileItem.style.fontSize = '10px';
// File icon // File icon using getFileIcon
const icon = document.createElement('span'); const iconWrapper = document.createElement('span');
icon.textContent = '📊'; iconWrapper.className = 'flex-shrink-0';
fileItem.appendChild(icon); iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
fileItem.appendChild(iconWrapper);
const fileName = document.createElement('span'); const fileName = document.createElement('span');
fileName.textContent = file.name; fileName.textContent = file.name;
@@ -2132,19 +2079,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer'; fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px'; fileItem.style.fontSize = '10px';
// File icon based on extension // File icon using getFileIcon
const ext = file.name.split('.').pop().toLowerCase(); const iconWrapper = document.createElement('span');
const icon = document.createElement('span'); iconWrapper.className = 'flex-shrink-0';
if(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) { iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
icon.textContent = '🖼️'; fileItem.appendChild(iconWrapper);
} else if(['pdf'].includes(ext)) {
icon.textContent = '📕';
} else if(['doc', 'docx'].includes(ext)) {
icon.textContent = '📝';
} else {
icon.textContent = '📄';
}
fileItem.appendChild(icon);
const fileName = document.createElement('span'); const fileName = document.createElement('span');
fileName.textContent = file.name; fileName.textContent = file.name;
@@ -2410,28 +2349,28 @@
const type = (contentType || '').toLowerCase(); const type = (contentType || '').toLowerCase();
if (ext === 'pdf' || type.includes('pdf')) if (ext === 'pdf' || type.includes('pdf'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none"><rect fill="#D32F2F" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">PDF</text></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none"><rect fill="#D32F2F" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">PDF</text></svg>';
// Show Word icon with PDF badge to indicate it will be converted // Show Word icon with PDF badge to indicate it will be converted
if (['doc', 'docx'].includes(ext) || type.includes('word')) if (['doc', 'docx'].includes(ext) || type.includes('word'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none"><rect fill="#2B579A" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">W</text></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none"><rect fill="#2B579A" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">W</text></svg>';
if (['xls', 'xlsx'].includes(ext) || type.includes('excel') || type.includes('spreadsheet')) if (['xls', 'xlsx'].includes(ext) || type.includes('excel') || type.includes('spreadsheet'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none"><rect fill="#217346" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">X</text></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none"><rect fill="#217346" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">X</text></svg>';
if (['ppt', 'pptx'].includes(ext) || type.includes('powerpoint') || type.includes('presentation')) if (['ppt', 'pptx'].includes(ext) || type.includes('powerpoint') || type.includes('presentation'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none"><rect fill="#D35230" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">P</text></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none"><rect fill="#D35230" width="24" height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">P</text></svg>';
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'].includes(ext) || type.includes('image')) if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'].includes(ext) || type.includes('image'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><path d="M21 15l-5-5L5 21"/></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><path d="M21 15l-5-5L5 21"/></svg>';
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext) || type.includes('zip') || type.includes('compressed')) if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext) || type.includes('zip') || type.includes('compressed'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-9h-3v4h3v-4zm0-3h-3v2h3V7z"/></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-9h-3v4h3v-4zm0-3h-3v2h3V7z"/></svg>';
if (['txt', 'csv', 'log'].includes(ext) || type.includes('text')) if (['txt', 'csv', 'log'].includes(ext) || type.includes('text'))
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/><path d="M16 13H8M16 17H8M10 9H8"/></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/><path d="M16 13H8M16 17H8M10 9H8"/></svg>';
return '<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/></svg>'; return '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/></svg>';
}; };
const canPreviewInline = (name, contentType) => { const canPreviewInline = (name, contentType) => {
@@ -3069,6 +3008,17 @@
const prefs = await loadUserPreferences(); const prefs = await loadUserPreferences();
console.log('Init: loaded prefs=', prefs); console.log('Init: loaded prefs=', prefs);
applyPrefsToFilters(prefs); applyPrefsToFilters(prefs);
// Proactively refresh Graph token on load
console.log('Init: refreshing Graph token');
await refreshGraphToken();
// Set up background token refresh every 50 minutes (tokens typically expire after 60 min)
setInterval(async () => {
console.log('Background: refreshing Graph token');
await refreshGraphToken();
}, 50 * 60 * 1000); // 50 minutes
console.log('Init: about to fetch jobs'); console.log('Init: about to fetch jobs');
fetchAllJobs(); fetchAllJobs();
})(); })();