made updates to manager info view

This commit is contained in:
2025-12-28 04:20:26 +00:00
parent 07d8f83883
commit cb9df953e1
+239 -31
View File
@@ -202,7 +202,7 @@
<div class="flex-1 relative bg-white flex flex-col overflow-hidden">
<div id="fileListContainer" class="hidden flex-1 flex flex-col p-0 bg-white overflow-hidden"></div>
<div id="pdfViewer" class="hidden flex-1 flex flex-col bg-gray-50">
<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white shadow-sm flex-shrink-0">
<div id="pdfControls" class="flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white shadow-sm flex-shrink-0">
<div class="flex items-center gap-1">
<button id="pdfZoomOut" class="px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded"></button>
<button id="pdfZoomReset" class="px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded">45%</button>
@@ -1032,7 +1032,7 @@
const col1 = document.createElement('div'); col1.className='flex-1';
const label1 = document.createElement('div'); label1.className='text-xs font-bold text-gray-700 uppercase tracking-wide mb-1'; label1.textContent='Manager';
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Manager'] || '—';
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Project_Manager'] || '—';
col1.appendChild(label1); col1.appendChild(val1);
const col2 = document.createElement('div'); col2.className='flex-1';
@@ -1145,7 +1145,7 @@
jobFullNameDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Job Full Name</div><div class="text-gray-900 truncate" style="font-size: 10px;" title="${job.Job_Full_Name || '—'}">${job.Job_Full_Name || '—'}</div>`;
const managerDiv = document.createElement('div');
managerDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Manager</div><div class="text-gray-900" style="font-size: 10px;">${job.Manager || '—'}</div>`;
managerDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Manager</div><div class="text-gray-900" style="font-size: 10px;">${job.Project_Manager || '—'}</div>`;
const jobStatusDiv = document.createElement('div');
jobStatusDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Job Status</div><div class="text-gray-900" style="font-size: 10px;">${job.Job_Status || '—'}</div>`;
@@ -1169,7 +1169,7 @@
jobType.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Job Type</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Job_Type || '—'}</div>`;
const scopeDiv = document.createElement('div');
scopeDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Scope</div><div class="text-gray-900 text-center" style="font-size: 10px;"></div>`;
scopeDiv.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Scope</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Scope || '—'}</div>`;
row2.appendChild(jobNum);
row2.appendChild(jobDivision);
@@ -1206,7 +1206,7 @@
contactNumber.appendChild(phoneDiv);
const extn = document.createElement('div');
extn.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EXT.</div><div class="text-gray-900 text-center" style="font-size: 10px;"></div>`;
extn.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EXT.</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.EXT || '—'}</div>`;
row3.appendChild(companyClient);
row3.appendChild(contactPerson);
@@ -1253,7 +1253,14 @@
// EST Cost in 4th column (centered)
const estimatedCostContainer = document.createElement('div');
estimatedCostContainer.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EST Cost</div><div class="text-gray-900 text-center" style="font-size: 10px;">—</div>`;
let costFormatted = '—';
if(job.Estimated_Cost !== null && job.Estimated_Cost !== undefined && job.Estimated_Cost !== '') {
const costNum = parseFloat(job.Estimated_Cost);
if(!isNaN(costNum)) {
costFormatted = '$' + costNum.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
estimatedCostContainer.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EST Cost</div><div class="text-gray-900 text-center" style="font-size: 10px;">${costFormatted}</div>`;
row4.appendChild(addressContainer);
row4.appendChild(mapIconsContainer);
@@ -1278,11 +1285,29 @@
const estimatedStartDate = document.createElement('div');
estimatedStartDate.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EST Start Date</div><div class="text-gray-900 text-center" style="font-size: 10px;">${startDateFormatted}</div>`;
let actualStartDateFormatted = '—';
if(job.Start_Date_Actual){
try {
const date = new Date(job.Start_Date_Actual);
actualStartDateFormatted = date.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' });
} catch(e) {
actualStartDateFormatted = job.Start_Date_Actual;
}
}
const actualStartDate = document.createElement('div');
actualStartDate.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Actual Start Date</div><div class="text-gray-900 text-center" style="font-size: 10px;">${startDateFormatted}</div>`;
actualStartDate.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Actual Start Date</div><div class="text-gray-900 text-center" style="font-size: 10px;">${actualStartDateFormatted}</div>`;
let estimatedEndDateFormatted = '—';
if(job.Estimated_End_Date){
try {
const date = new Date(job.Estimated_End_Date);
estimatedEndDateFormatted = date.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' });
} catch(e) {
estimatedEndDateFormatted = job.Estimated_End_Date;
}
}
const estimatedEndDate = document.createElement('div');
estimatedEndDate.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EST End Date</div><div class="text-gray-900 text-center" style="font-size: 10px;"></div>`;
estimatedEndDate.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">EST End Date</div><div class="text-gray-900 text-center" style="font-size: 10px;">${estimatedEndDateFormatted}</div>`;
row5.appendChild(estimatedStartDate);
row5.appendChild(actualStartDate);
@@ -1441,6 +1466,12 @@
<button data-cmd="foreColor" data-val="#0000ff" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-blue-600" style="font-size: 8px;">A</button>
<button data-cmd="foreColor" data-val="#000000" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-black" style="font-size: 8px;">A</button>
<button data-cmd="removeFormat" class="h-6 px-2 rounded border border-gray-400 bg-gray-400 text-white cursor-pointer flex items-center justify-center" style="font-size: 8px;">Reset</button>
<button id="jobNoteMic" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 cursor-pointer flex items-center justify-center" title="Voice to text">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="#4B5563">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
</button>
</div>
<div id="jobNewNote" contenteditable="true" class="min-h-14 max-h-[120px] overflow-auto p-2 border border-gray-600 rounded text-sm bg-white mb-2"></div>
<div class="flex gap-2">
@@ -1505,6 +1536,12 @@
<button data-cmd="foreColor" data-val="#0000ff" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-blue-600" style="font-size: 8px;">A</button>
<button data-cmd="foreColor" data-val="#000000" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-black" style="font-size: 8px;">A</button>
<button data-cmd="removeFormat" class="h-6 px-2 rounded border border-gray-400 bg-gray-400 text-white cursor-pointer flex items-center justify-center" style="font-size: 8px;">Reset</button>
<button id="managerNoteMic" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 cursor-pointer flex items-center justify-center" title="Voice to text">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="#4B5563">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
</button>
</div>
<div id="managerNewNote" contenteditable="true" class="min-h-14 max-h-[120px] overflow-auto p-2 border border-gray-600 rounded text-sm bg-white mb-2"></div>
<div class="flex gap-2">
@@ -1597,12 +1634,21 @@
populateNotesFileColumns([], 'managerNotesFileColumns');
}
// Load and display notes filtered by type
loadAndDisplayNotes(job);
// Setup toolbar functionality for job notes editor
setupNoteEditorToolbar('jobNewNote', 'jobNoteEditor');
// Setup toolbar functionality for manager notes editor
setupNoteEditorToolbar('managerNewNote', 'managerNoteEditor');
// Setup voice-to-text for job notes
setupVoiceToText('jobNoteMic', 'jobNewNote');
// Setup voice-to-text for manager notes
setupVoiceToText('managerNoteMic', 'managerNewNote');
// Setup close functionality
document.getElementById('jobNoteCollapse').addEventListener('click', (e) => {
console.log('Close job note editor');
@@ -1668,6 +1714,102 @@
}
}
// Setup voice-to-text functionality
function setupVoiceToText(micButtonId, editorId) {
const micBtn = document.getElementById(micButtonId);
const editor = document.getElementById(editorId);
if(!micBtn || !editor) {
console.log('Mic button or editor not found:', micButtonId, editorId);
return;
}
// Check for browser support
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if(!SpeechRecognition) {
micBtn.disabled = true;
micBtn.title = 'Speech recognition not supported';
micBtn.style.opacity = '0.5';
return;
}
const recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
let isListening = false;
const micIcon = micBtn.querySelector('svg');
micBtn.addEventListener('click', (e) => {
e.preventDefault();
if(isListening) {
recognition.stop();
isListening = false;
micIcon.setAttribute('fill', '#4B5563'); // Gray (inactive)
micBtn.style.backgroundColor = '';
} else {
try {
recognition.start();
isListening = true;
micIcon.setAttribute('fill', '#EF4444'); // Red (recording)
micBtn.style.backgroundColor = '#FEE2E2'; // Light red background
} catch(err) {
console.error('Recognition start error:', err);
}
}
});
recognition.onresult = (event) => {
let interimTranscript = '';
let finalTranscript = '';
for(let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if(event.results[i].isFinal) {
finalTranscript += transcript + ' ';
} else {
interimTranscript += transcript;
}
}
if(finalTranscript) {
// Insert final transcript at cursor position
editor.focus();
const selection = window.getSelection();
if(selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(finalTranscript));
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
} else {
// Fallback: append to end
editor.innerHTML += finalTranscript;
}
}
};
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
isListening = false;
micIcon.setAttribute('fill', '#4B5563');
micBtn.style.backgroundColor = '';
if(event.error === 'not-allowed') {
alert('Microphone access denied. Please allow microphone access in your browser settings.');
}
};
recognition.onend = () => {
isListening = false;
micIcon.setAttribute('fill', '#4B5563');
micBtn.style.backgroundColor = '';
};
}
// Setup toolbar for note editors
function setupNoteEditorToolbar(editorId, containerId) {
const editor = document.getElementById(editorId);
@@ -1756,30 +1898,40 @@
hour12: true
});
const currentUserName = getDisplayName();
const noteType = isManagerNote ? '[MANAGER NOTE]' : '';
const formatted = `${noteType}[${currentUserName} - ${timestamp}] [Job #${job?.Job_Number||'N/A'}] ${html}\n\n`;
const updatedNotes = formatted + (job.Notes || '');
const noteType = isManagerNote ? 'manager' : 'job';
const typeLabel = isManagerNote ? '[MANAGER NOTE]' : '';
const formatted = `${typeLabel}[${currentUserName} - ${timestamp}] [Job #${job?.Job_Number||'N/A'}] ${html}`;
try {
const payloadNote = {
Note: formatted.trim(),
Job_Number: job?.Job_Number || '',
Job_Number_Id: job?.id ? [job.id] : []
Job_Number_Id: job?.id ? [job.id] : [],
type: noteType
};
// Update job record
const resJob = await fetch(`${PB_COLLECTION_URL}/${job.id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json', ...authHeaders()},
body: JSON.stringify({Notes: updatedNotes})
});
// 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;
}
if(!resJob.ok) throw new Error('Job_Info update failed: ' + resJob.status);
const updated = await resJob.json();
const jobIdToLink = updated.id || job.id;
// Create note in notes collection
payloadNote.Job_Number_Id = [jobIdToLink];
// Create note in notes collection with type
const resNote = await fetch(`${NOTES_COLLECTION_URL}`, {
method: 'POST',
headers: {'Content-Type': 'application/json', ...authHeaders()},
@@ -1787,17 +1939,23 @@
});
if(!resNote.ok) {
console.warn('Note creation failed with status', resNote.status);
const errText = await resNote.text().catch(() => '');
console.error('Note creation failed:', resNote.status, errText);
// Don't throw if Notes collection fails but Job_Info succeeded
if(!isManagerNote) {
console.warn('Note saved to Job_Info but failed to save to Notes collection');
} else {
throw new Error('Failed to save note: ' + resNote.status);
}
}
// Update UI
currentJob.Notes = updatedNotes;
// Clear editor
editor.innerHTML = '';
alert('Note saved successfully!');
// Refresh the display
if(currentJob) {
populateManagerInfo(currentJob);
await loadAndDisplayNotes(currentJob);
}
} catch(err) {
console.error('Error saving note:', err);
@@ -1805,6 +1963,55 @@
}
}
// Load and display notes filtered by type
async function loadAndDisplayNotes(job) {
if(!job?.id) return;
try {
// Fetch all notes for this job
const filter = `Job_Number_Id?~"${job.id}"`;
const resp = await fetch(`${NOTES_COLLECTION_URL}?filter=${encodeURIComponent(filter)}&sort=-created`, {
headers: authHeaders()
});
if(!resp.ok) {
console.error('Failed to fetch notes:', resp.status);
return;
}
const data = await resp.json();
const allNotes = data.items || [];
// Filter notes by type
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');
if(jobNotesDisplay) {
if(jobNotes.length > 0) {
jobNotesDisplay.innerHTML = jobNotes.map(n => n.Note).join('\n\n');
} else {
jobNotesDisplay.innerHTML = '<span class="text-gray-400 italic">No notes available</span>';
}
}
// Update manager notes display
const managerNotesDisplay = document.querySelector('#managerNotesContent > div:first-child');
if(managerNotesDisplay) {
if(managerNotes.length > 0) {
managerNotesDisplay.innerHTML = managerNotes.map(n => n.Note).join('\n\n');
} else {
managerNotesDisplay.innerHTML = '<span class="text-gray-400 italic">No manager notes available</span>';
}
}
console.log('Loaded notes - Job:', jobNotes.length, 'Manager:', managerNotes.length);
} catch(err) {
console.error('Error loading notes:', err);
}
}
// Function to populate file columns in notes section
function populateNotesFileColumns(files, containerId) {
const container = document.getElementById(containerId);
@@ -1855,7 +2062,7 @@
fileName.className = 'truncate';
fileItem.appendChild(fileName);
fileItem.onclick = () => openFilePreview(file);
fileItem.onclick = () => openFilePreview(file.driveId || '', file.id || '', file.name, file.contentType || '', file.url);
plansList.appendChild(fileItem);
});
} else {
@@ -1884,7 +2091,7 @@
fileName.className = 'truncate';
fileItem.appendChild(fileName);
fileItem.onclick = () => openFilePreview(file);
fileItem.onclick = () => openFilePreview(file.driveId || '', file.id || '', file.name, file.contentType || '', file.url);
estimatesList.appendChild(fileItem);
});
} else {
@@ -1922,7 +2129,7 @@
fileName.className = 'truncate';
fileItem.appendChild(fileName);
fileItem.onclick = () => openFilePreview(file);
fileItem.onclick = () => openFilePreview(file.driveId || '', file.id || '', file.name, file.contentType || '', file.url);
otherList.appendChild(fileItem);
});
} else {
@@ -2105,6 +2312,7 @@
let pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
const pdfViewer = document.getElementById('pdfViewer');
const pdfCanvas = document.getElementById('pdfCanvas');
const pdfControls = document.getElementById('pdfControls');
const pdfPageNum = document.getElementById('pdfPageNum');
const pdfPageCount = document.getElementById('pdfPageCount');
const pdfPrev = document.getElementById('pdfPrev');