Compare commits

...

2 Commits

Author SHA1 Message Date
aewing dd0eba8aee 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
2025-12-28 05:55:03 +00:00
aewing eead6de627 manager info view job notes working correctly now. 2025-12-28 04:54:32 +00:00
+130 -158
View File
@@ -698,6 +698,12 @@
// Remove trailing punctuation (period, comma, etc.)
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)
transcript = transcript.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
@@ -721,6 +727,16 @@
// Trigger search with results
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
const allFinal = Array.from(event.results).every(r => r.isFinal);
if (allFinal && event.results.length > 0) {
@@ -770,6 +786,12 @@
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
if (!hasPermission) {
const granted = await requestMicPermission();
@@ -1314,102 +1336,6 @@
row5.appendChild(estimatedEndDate);
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
const notesSection = document.createElement('div');
notesSection.className = 'mt-4';
@@ -1439,12 +1365,21 @@
jobNotesContent.id = 'jobNotesContent';
jobNotesContent.className = 'notes-tab-content';
// Container for display with expand button
const jobNotesDisplayContainer = document.createElement('div');
jobNotesDisplayContainer.className = 'relative mb-3';
// Display notes first
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.height = '120px';
jobNotesDisplay.style.maxHeight = '120px';
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
const addNoteBtn = document.createElement('button');
@@ -1509,12 +1444,21 @@
managerNotesContent.id = 'managerNotesContent';
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
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.height = '120px';
managerNotesDisplay.style.maxHeight = '120px';
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
const addManagerNoteBtn = document.createElement('button');
@@ -1899,37 +1843,56 @@
});
const currentUserName = getDisplayName();
const noteType = isManagerNote ? 'manager' : 'job';
const typeLabel = isManagerNote ? '[MANAGER NOTE]' : '';
const formatted = `${typeLabel}[${currentUserName} - ${timestamp}] [Job #${job?.Job_Number||'N/A'}] ${html}`;
const typeLabel = isManagerNote ? '[MANAGER]' : '';
const formatted = `${typeLabel}[${currentUserName} - ${timestamp}] ${html}`;
try {
// If it's a job note, first update Job_Info_Prod.Notes field
let jobRecordId = job?.id || '';
if(!isManagerNote) {
// Fetch the latest notes from PocketBase to ensure we have the full chain
const fetchLatest = await fetch(`${PB_COLLECTION_URL}/${job.id}`, {
headers: authHeaders()
});
if(fetchLatest.ok) {
const latestJob = await fetchLatest.json();
const currentNotes = latestJob.Notes || '';
const updatedNotes = formatted + '\n\n' + currentNotes;
const resJob = await fetch(`${PB_COLLECTION_URL}/${job.id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json', ...authHeaders()},
body: JSON.stringify({Notes: updatedNotes})
});
if(!resJob.ok) {
console.error('Job_Info update failed:', resJob.status);
throw new Error('Failed to update Job_Info: ' + resJob.status);
}
const updated = await resJob.json();
jobRecordId = updated.id || job.id;
// Update both current job object and the passed job parameter
currentJob.Notes = updatedNotes;
job.Notes = updatedNotes;
console.log('Updated Job_Info_Prod.Notes for job:', jobRecordId);
} else {
throw new Error('Failed to fetch latest job data: ' + fetchLatest.status);
}
}
// Create payload - only include job_info_id, not Job_Number_Id array
const payloadNote = {
Note: formatted.trim(),
Job_Number: job?.Job_Number || '',
Job_Number_Id: job?.id ? [job.id] : [],
job_info_id: jobRecordId,
type: noteType
};
// If it's a job note, also update Job_Info_Prod.Notes field
if(!isManagerNote) {
const updatedNotes = formatted + '\n\n' + (job.Notes || '');
const resJob = await fetch(`${PB_COLLECTION_URL}/${job.id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json', ...authHeaders()},
body: JSON.stringify({Notes: updatedNotes})
});
if(!resJob.ok) {
console.error('Job_Info update failed:', resJob.status);
throw new Error('Failed to update Job_Info: ' + resJob.status);
}
const updated = await resJob.json();
payloadNote.Job_Number_Id = [updated.id || job.id];
// Update current job object
currentJob.Notes = updatedNotes;
}
console.log('Submitting note payload:', payloadNote);
// Create note in notes collection with type
const resNote = await fetch(`${NOTES_COLLECTION_URL}`, {
@@ -1947,6 +1910,9 @@
} else {
throw new Error('Failed to save note: ' + resNote.status);
}
} else {
const savedNote = await resNote.json();
console.log('Note created successfully in Notes collection:', savedNote);
}
// Clear editor
@@ -1968,8 +1934,8 @@
if(!job?.id) return;
try {
// Fetch all notes for this job
const filter = `Job_Number_Id?~"${job.id}"`;
// Fetch all notes for this job using job_info_id
const filter = `job_info_id="${job.id}"`;
const resp = await fetch(`${NOTES_COLLECTION_URL}?filter=${encodeURIComponent(filter)}&sort=-created`, {
headers: authHeaders()
});
@@ -1986,8 +1952,8 @@
const jobNotes = allNotes.filter(n => !n.type || n.type === 'job');
const managerNotes = allNotes.filter(n => n.type === 'manager');
// Update job notes display
const jobNotesDisplay = document.querySelector('#jobNotesContent > div:first-child');
// Update job notes display using ID selector
const jobNotesDisplay = document.getElementById('jobNotesDisplay');
if(jobNotesDisplay) {
if(jobNotes.length > 0) {
jobNotesDisplay.innerHTML = jobNotes.map(n => n.Note).join('\n\n');
@@ -1996,8 +1962,8 @@
}
}
// Update manager notes display
const managerNotesDisplay = document.querySelector('#managerNotesContent > div:first-child');
// Update manager notes display using ID selector
const managerNotesDisplay = document.getElementById('managerNotesDisplay');
if(managerNotesDisplay) {
if(managerNotes.length > 0) {
managerNotesDisplay.innerHTML = managerNotes.map(n => n.Note).join('\n\n');
@@ -2039,6 +2005,7 @@
otherFiles.push(file);
}
});
console.log('Categorized - Plans:', plans.length, 'Estimates:', estimates.length, 'Other:', otherFiles.length);
}
// Plans column
@@ -2052,10 +2019,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px';
// File icon
const icon = document.createElement('span');
icon.textContent = '📄';
fileItem.appendChild(icon);
// File icon using getFileIcon
const iconWrapper = document.createElement('span');
iconWrapper.className = 'flex-shrink-0';
iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
fileItem.appendChild(iconWrapper);
const fileName = document.createElement('span');
fileName.textContent = file.name;
@@ -2081,10 +2049,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px';
// File icon
const icon = document.createElement('span');
icon.textContent = '📊';
fileItem.appendChild(icon);
// File icon using getFileIcon
const iconWrapper = document.createElement('span');
iconWrapper.className = 'flex-shrink-0';
iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
fileItem.appendChild(iconWrapper);
const fileName = document.createElement('span');
fileName.textContent = file.name;
@@ -2110,19 +2079,11 @@
fileItem.className = 'flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer';
fileItem.style.fontSize = '10px';
// File icon based on extension
const ext = file.name.split('.').pop().toLowerCase();
const icon = document.createElement('span');
if(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) {
icon.textContent = '🖼️';
} else if(['pdf'].includes(ext)) {
icon.textContent = '📕';
} else if(['doc', 'docx'].includes(ext)) {
icon.textContent = '📝';
} else {
icon.textContent = '📄';
}
fileItem.appendChild(icon);
// File icon using getFileIcon
const iconWrapper = document.createElement('span');
iconWrapper.className = 'flex-shrink-0';
iconWrapper.innerHTML = getFileIcon(file.name, file.contentType);
fileItem.appendChild(iconWrapper);
const fileName = document.createElement('span');
fileName.textContent = file.name;
@@ -2388,28 +2349,28 @@
const type = (contentType || '').toLowerCase();
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
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'))
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'))
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'))
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'))
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'))
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) => {
@@ -3047,6 +3008,17 @@
const prefs = await loadUserPreferences();
console.log('Init: loaded prefs=', 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');
fetchAllJobs();
})();