@@ -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 = `
Job Full Name
${job.Job_Full_Name || '—'}
`;
const managerDiv = document.createElement('div');
- managerDiv.innerHTML = `
Manager
${job.Manager || '—'}
`;
+ managerDiv.innerHTML = `
Manager
${job.Project_Manager || '—'}
`;
const jobStatusDiv = document.createElement('div');
jobStatusDiv.innerHTML = `
Job Status
${job.Job_Status || '—'}
`;
@@ -1169,7 +1169,7 @@
jobType.innerHTML = `
Job Type
${job.Job_Type || '—'}
`;
const scopeDiv = document.createElement('div');
- scopeDiv.innerHTML = `
Scope
—
`;
+ scopeDiv.innerHTML = `
Scope
${job.Scope || '—'}
`;
row2.appendChild(jobNum);
row2.appendChild(jobDivision);
@@ -1206,7 +1206,7 @@
contactNumber.appendChild(phoneDiv);
const extn = document.createElement('div');
- extn.innerHTML = `
EXT.
—
`;
+ extn.innerHTML = `
EXT.
${job.EXT || '—'}
`;
row3.appendChild(companyClient);
row3.appendChild(contactPerson);
@@ -1253,7 +1253,14 @@
// EST Cost in 4th column (centered)
const estimatedCostContainer = document.createElement('div');
- estimatedCostContainer.innerHTML = `
EST Cost
—
`;
+ 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 = `
EST Cost
${costFormatted}
`;
row4.appendChild(addressContainer);
row4.appendChild(mapIconsContainer);
@@ -1278,11 +1285,29 @@
const estimatedStartDate = document.createElement('div');
estimatedStartDate.innerHTML = `
EST Start Date
${startDateFormatted}
`;
+ 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 = `
Actual Start Date
${startDateFormatted}
`;
+ actualStartDate.innerHTML = `
Actual Start Date
${actualStartDateFormatted}
`;
+ 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 = `
EST End Date
—
`;
+ estimatedEndDate.innerHTML = `
EST End Date
${estimatedEndDateFormatted}
`;
row5.appendChild(estimatedStartDate);
row5.appendChild(actualStartDate);
@@ -1441,6 +1466,12 @@
+
@@ -1505,6 +1536,12 @@
+
@@ -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 = 'No notes available';
+ }
+ }
+
+ // 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 = 'No manager notes available';
+ }
+ }
+
+ 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');