added manager info view

This commit is contained in:
2025-12-28 03:10:53 +00:00
parent a191db8449
commit 07d8f83883
+837 -1
View File
@@ -159,6 +159,14 @@
</div> </div>
</div> </div>
<!-- MANAGER INFO Tab Content -->
<div id="managerInfoContent" class="hidden flex-1 overflow-hidden flex flex-col min-h-0">
<div id="managerInfoHeading" class="bg-green-600 text-white text-center py-2 text-sm font-semibold rounded-t-lg flex-shrink-0">MANAGER INFO</div>
<div class="px-3 pt-2 pb-1 flex-1 overflow-auto">
<div id="managerInfoTable" class="w-full space-y-3"></div>
</div>
</div>
<!-- Tab Bar at Bottom --> <!-- Tab Bar at Bottom -->
<div class="flex items-center border-t border-gray-200 pt-2 px-3 pb-2 flex-shrink-0 bg-white rounded-b-lg"> <div class="flex items-center border-t border-gray-200 pt-2 px-3 pb-2 flex-shrink-0 bg-white rounded-b-lg">
<button id="backBtn" class="mr-6 pr-6 border-r border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center"> <button id="backBtn" class="mr-6 pr-6 border-r border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center">
@@ -169,6 +177,7 @@
</button> </button>
<button id="infoTab" class="px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px">INFO</button> <button id="infoTab" class="px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px">INFO</button>
<button id="notesTab" class="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">NOTES</button> <button id="notesTab" class="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">NOTES</button>
<button id="managerInfoTab" class="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">MANAGER INFO</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1122,6 +1131,810 @@
newNote.innerHTML = ''; newNote.innerHTML = '';
tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600')); tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600'));
} }
async function populateManagerInfo(job) {
const managerInfoTable = document.getElementById('managerInfoTable');
managerInfoTable.innerHTML = '';
// Row 1: Job Full Name (spans 2 cols), Manager, Job Status
const row1 = document.createElement('div');
row1.className = 'grid grid-cols-4 gap-4 pb-3 border-b border-gray-200';
const jobFullNameDiv = document.createElement('div');
jobFullNameDiv.className = 'col-span-2';
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>`;
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>`;
row1.appendChild(jobFullNameDiv);
row1.appendChild(managerDiv);
row1.appendChild(jobStatusDiv);
managerInfoTable.appendChild(row1);
// Row 2: Job Number, Job Division, Job Type, Scope
const row2 = document.createElement('div');
row2.className = 'grid grid-cols-4 gap-4 pb-3 border-b border-gray-200';
const jobNum = document.createElement('div');
jobNum.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Job Number</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Job_Number || '—'}</div>`;
const jobDivision = document.createElement('div');
jobDivision.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Job Division</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Job_Division || '—'}</div>`;
const jobType = document.createElement('div');
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>`;
row2.appendChild(jobNum);
row2.appendChild(jobDivision);
row2.appendChild(jobType);
row2.appendChild(scopeDiv);
managerInfoTable.appendChild(row2);
// Row 3: Company/Client, Contact Person, Contact Number, EXT.
const row3 = document.createElement('div');
row3.className = 'grid grid-cols-4 gap-4 pb-3 border-b border-gray-200';
const companyClient = document.createElement('div');
companyClient.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Company/Client</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Company_Client || '—'}</div>`;
const contactPerson = document.createElement('div');
contactPerson.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Contact Person</div><div class="text-gray-900 text-center" style="font-size: 10px;">${job.Contact_Person || '—'}</div>`;
const contactNumber = document.createElement('div');
contactNumber.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1 text-center" style="font-size: 8px;">Contact Number</div>`;
const phoneDiv = document.createElement('div');
phoneDiv.className = 'text-center';
phoneDiv.style.fontSize = '10px';
if(job.Phone_Number && job.Phone_Number.trim() !== ''){
const phoneLink = document.createElement('a');
const phoneNumber = job.Phone_Number.replace(/\D/g, '');
phoneLink.href = `tel:${phoneNumber}`;
phoneLink.className = 'text-blue-600 hover:text-blue-800 underline';
phoneLink.textContent = job.Phone_Number;
phoneDiv.appendChild(phoneLink);
} else {
phoneDiv.className = 'text-gray-900 text-center';
phoneDiv.textContent = '—';
}
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>`;
row3.appendChild(companyClient);
row3.appendChild(contactPerson);
row3.appendChild(contactNumber);
row3.appendChild(extn);
managerInfoTable.appendChild(row3);
// Row 4: Job Address (spans 2 cols), Map Icons (3rd col), EST Cost (4th col)
const row4 = document.createElement('div');
row4.className = 'grid grid-cols-4 gap-4 pb-3 border-b border-gray-200';
// Job Address spanning 2 columns
const addressContainer = document.createElement('div');
addressContainer.className = 'col-span-2';
addressContainer.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Job Address</div>`;
const addressVal = document.createElement('div');
addressVal.style.fontSize = '10px';
addressVal.className = 'text-gray-900';
addressVal.textContent = job.Job_Address || '—';
addressContainer.appendChild(addressVal);
// Map Icons in 3rd column
const mapIconsContainer = document.createElement('div');
mapIconsContainer.className = 'flex items-end gap-2';
if(job.Job_Address && job.Job_Address.trim() !== ''){
const appleLink = document.createElement('a');
const encodedAddress = encodeURIComponent(job.Job_Address);
appleLink.href = `https://maps.apple.com/?q=${encodedAddress}`;
appleLink.target = '_blank';
appleLink.className = 'inline-flex items-center justify-center w-5 h-5 hover:opacity-80 transition-opacity';
appleLink.title = 'Open in Apple Maps';
appleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" style="fill: #000000;"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>';
mapIconsContainer.appendChild(appleLink);
const googleLink = document.createElement('a');
googleLink.href = `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
googleLink.target = '_blank';
googleLink.className = 'inline-flex items-center justify-center w-5 h-5 hover:opacity-80 transition-opacity';
googleLink.title = 'Open in Google Maps';
googleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" style="fill: #4285F4;"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>';
mapIconsContainer.appendChild(googleLink);
}
// 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>`;
row4.appendChild(addressContainer);
row4.appendChild(mapIconsContainer);
row4.appendChild(estimatedCostContainer);
managerInfoTable.appendChild(row4);
// Row 5: EST Start Date, Actual Start Date, EST End Date
const row5 = document.createElement('div');
row5.className = 'grid grid-cols-3 gap-4 pb-3 border-b border-gray-200';
// Format the start date for both estimated and actual (short format)
let startDateFormatted = '—';
if(job.Start_Date){
try {
const date = new Date(job.Start_Date);
startDateFormatted = date.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' });
} catch(e) {
startDateFormatted = job.Start_Date;
}
}
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>`;
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>`;
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>`;
row5.appendChild(estimatedStartDate);
row5.appendChild(actualStartDate);
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';
// Tab buttons
const notesTabs = document.createElement('div');
notesTabs.className = 'flex gap-2 mb-2';
const jobNotesTabBtn = document.createElement('button');
jobNotesTabBtn.id = 'jobNotesTab';
jobNotesTabBtn.className = 'px-3 py-1 font-bold uppercase bg-orange-600 text-white rounded cursor-pointer';
jobNotesTabBtn.style.fontSize = '8px';
jobNotesTabBtn.textContent = 'Notes';
const managerNotesTabBtn = document.createElement('button');
managerNotesTabBtn.id = 'managerNotesTab';
managerNotesTabBtn.className = 'px-3 py-1 font-bold uppercase bg-gray-300 text-gray-700 rounded hover:bg-gray-400 cursor-pointer';
managerNotesTabBtn.style.fontSize = '8px';
managerNotesTabBtn.textContent = 'Manager Notes';
notesTabs.appendChild(jobNotesTabBtn);
notesTabs.appendChild(managerNotesTabBtn);
notesSection.appendChild(notesTabs);
// Job Notes Content
const jobNotesContent = document.createElement('div');
jobNotesContent.id = 'jobNotesContent';
jobNotesContent.className = 'notes-tab-content';
// 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.style.fontSize = '10px';
jobNotesDisplay.innerHTML = job.Notes || '<span class="text-gray-400 italic">No notes available</span>';
jobNotesContent.appendChild(jobNotesDisplay);
// Add Note button below notes display
const addNoteBtn = document.createElement('button');
addNoteBtn.className = 'mb-2 px-3 py-1 bg-green-600 text-white rounded font-bold hover:bg-green-700 cursor-pointer';
addNoteBtn.style.fontSize = '8px';
addNoteBtn.textContent = 'Add A Note';
jobNotesContent.appendChild(addNoteBtn);
// Note editor (collapsed by default)
const noteEditor = document.createElement('div');
noteEditor.id = 'jobNoteEditor';
noteEditor.className = 'hidden mb-3 p-2 border border-gray-300 rounded bg-gray-50';
noteEditor.innerHTML = `
<div class="flex flex-wrap gap-1 mb-2">
<button data-cmd="bold" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><strong>B</strong></button>
<button data-cmd="italic" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><em>I</em></button>
<button data-cmd="underline" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-red-500" style="font-size: 8px;">A</button>
<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>
</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">
<button id="jobNoteSubmit" class="px-3 py-1 rounded text-white bg-green-600 hover:bg-green-700 font-bold cursor-pointer" style="font-size: 8px;">Submit</button>
<button id="jobNoteClear" class="px-3 py-1 rounded text-white bg-gray-500 hover:bg-gray-600 font-bold cursor-pointer" style="font-size: 8px;">Clear</button>
<button id="jobNoteCollapse" class="px-3 py-1 rounded text-white bg-red-600 hover:bg-red-700 font-bold cursor-pointer" style="font-size: 8px;">Close</button>
</div>
`;
jobNotesContent.appendChild(noteEditor);
// File columns - always create them
const fileColumns = document.createElement('div');
fileColumns.id = 'jobNotesFileColumns';
fileColumns.className = 'grid grid-cols-3 gap-4 mt-3';
// Initialize with empty state
fileColumns.innerHTML = `
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Plans</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Estimates</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Other Files</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
`;
jobNotesContent.appendChild(fileColumns);
// Manager Notes Content
const managerNotesContent = document.createElement('div');
managerNotesContent.id = 'managerNotesContent';
managerNotesContent.className = 'notes-tab-content hidden';
// 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.style.fontSize = '10px';
managerNotesDisplay.innerHTML = '<span class="text-gray-400 italic">No manager notes available</span>';
managerNotesContent.appendChild(managerNotesDisplay);
// Add Note button for manager below notes display
const addManagerNoteBtn = document.createElement('button');
addManagerNoteBtn.className = 'mb-2 px-3 py-1 bg-green-600 text-white rounded font-bold hover:bg-green-700 cursor-pointer';
addManagerNoteBtn.style.fontSize = '8px';
addManagerNoteBtn.textContent = 'Add A Note';
managerNotesContent.appendChild(addManagerNoteBtn);
// Manager note editor (collapsed by default)
const managerNoteEditor = document.createElement('div');
managerNoteEditor.id = 'managerNoteEditor';
managerNoteEditor.className = 'hidden mb-3 p-2 border border-gray-300 rounded bg-gray-50';
managerNoteEditor.innerHTML = `
<div class="flex flex-wrap gap-1 mb-2">
<button data-cmd="bold" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><strong>B</strong></button>
<button data-cmd="italic" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><em>I</em></button>
<button data-cmd="underline" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center" style="font-size: 8px;"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" class="h-6 w-8 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center text-red-500" style="font-size: 8px;">A</button>
<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>
</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">
<button id="managerNoteSubmit" class="px-3 py-1 rounded text-white bg-green-600 hover:bg-green-700 font-bold cursor-pointer" style="font-size: 8px;">Submit</button>
<button id="managerNoteClear" class="px-3 py-1 rounded text-white bg-gray-500 hover:bg-gray-600 font-bold cursor-pointer" style="font-size: 8px;">Clear</button>
<button id="managerNoteCollapse" class="px-3 py-1 rounded text-white bg-red-600 hover:bg-red-700 font-bold cursor-pointer" style="font-size: 8px;">Close</button>
</div>
`;
managerNotesContent.appendChild(managerNoteEditor);
// File columns for manager notes - always create them
const managerFileColumns = document.createElement('div');
managerFileColumns.id = 'managerNotesFileColumns';
managerFileColumns.className = 'grid grid-cols-3 gap-4 mt-3';
// Initialize with empty state
managerFileColumns.innerHTML = `
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Plans</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Estimates</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
<div>
<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Other Files</div>
<div class="text-gray-400 italic" style="font-size: 10px;">Loading...</div>
</div>
`;
managerNotesContent.appendChild(managerFileColumns);
notesSection.appendChild(jobNotesContent);
notesSection.appendChild(managerNotesContent);
managerInfoTable.appendChild(notesSection);
// Tab switching event listeners
jobNotesTabBtn.addEventListener('click', (e) => {
console.log('Notes tab clicked');
e.preventDefault();
e.stopPropagation();
jobNotesTabBtn.className = 'px-3 py-1 text-xs font-bold uppercase bg-orange-600 text-white rounded cursor-pointer';
managerNotesTabBtn.className = 'px-3 py-1 text-xs font-bold uppercase bg-gray-300 text-gray-700 rounded hover:bg-gray-400 cursor-pointer';
jobNotesContent.classList.remove('hidden');
managerNotesContent.classList.add('hidden');
});
managerNotesTabBtn.addEventListener('click', (e) => {
console.log('Manager Notes tab clicked');
e.preventDefault();
e.stopPropagation();
managerNotesTabBtn.className = 'px-3 py-1 text-xs font-bold uppercase bg-orange-600 text-white rounded cursor-pointer';
jobNotesTabBtn.className = 'px-3 py-1 text-xs font-bold uppercase bg-gray-300 text-gray-700 rounded hover:bg-gray-400 cursor-pointer';
managerNotesContent.classList.remove('hidden');
jobNotesContent.classList.add('hidden');
});
// Note editor controls
addNoteBtn.addEventListener('click', (e) => {
console.log('Add Note clicked');
e.preventDefault();
addNoteBtn.classList.add('hidden');
noteEditor.classList.remove('hidden');
});
addManagerNoteBtn.addEventListener('click', (e) => {
console.log('Add Manager Note clicked');
e.preventDefault();
addManagerNoteBtn.classList.add('hidden');
managerNoteEditor.classList.remove('hidden');
});
// Populate file columns
if(job.Job_Folder_Link && String(job.Job_Folder_Link).trim() !== ''){
console.log('Fetching files for notes sections...');
loadFilesForNotes(job.Job_Folder_Link).then(files => {
console.log('Files fetched, populating columns:', files.length);
populateNotesFileColumns(files, 'jobNotesFileColumns');
populateNotesFileColumns(files, 'managerNotesFileColumns');
}).catch(err => {
console.log('Error loading files for notes:', err);
// Show empty state on error
populateNotesFileColumns([], 'jobNotesFileColumns');
populateNotesFileColumns([], 'managerNotesFileColumns');
});
} else {
console.log('No Job_Folder_Link, showing empty state');
// Show empty state when no folder link
populateNotesFileColumns([], 'jobNotesFileColumns');
populateNotesFileColumns([], 'managerNotesFileColumns');
}
// Setup toolbar functionality for job notes editor
setupNoteEditorToolbar('jobNewNote', 'jobNoteEditor');
// Setup toolbar functionality for manager notes editor
setupNoteEditorToolbar('managerNewNote', 'managerNoteEditor');
// Setup close functionality
document.getElementById('jobNoteCollapse').addEventListener('click', (e) => {
console.log('Close job note editor');
e.preventDefault();
noteEditor.classList.add('hidden');
addNoteBtn.classList.remove('hidden');
});
document.getElementById('managerNoteCollapse').addEventListener('click', (e) => {
console.log('Close manager note editor');
e.preventDefault();
managerNoteEditor.classList.add('hidden');
addManagerNoteBtn.classList.remove('hidden');
});
// Setup submit functionality
document.getElementById('jobNoteSubmit').addEventListener('click', async(e) => {
e.preventDefault();
await submitNote('jobNewNote', job, false);
noteEditor.classList.add('hidden');
addNoteBtn.classList.remove('hidden');
});
document.getElementById('jobNoteClear').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('jobNewNote').innerHTML = '';
});
document.getElementById('managerNoteSubmit').addEventListener('click', async(e) => {
e.preventDefault();
await submitNote('managerNewNote', job, true);
managerNoteEditor.classList.add('hidden');
addManagerNoteBtn.classList.remove('hidden');
});
document.getElementById('managerNoteClear').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('managerNewNote').innerHTML = '';
});
}
// Load files for notes sections
async function loadFilesForNotes(folderLink) {
console.log('loadFilesForNotes called with:', folderLink);
const graphToken = await ensureGraphToken();
const headers = graphToken ? { 'x-graph-token': graphToken } : {};
try {
const resp = await fetch(`/api/job-files?link=${encodeURIComponent(folderLink)}`, { headers });
if(!resp.ok) {
console.error('Failed to fetch files:', resp.status, resp.statusText);
return [];
}
const data = await resp.json();
console.log('Files loaded:', data.items?.length || 0);
return data.items || [];
} catch(err) {
console.error('Error in loadFilesForNotes:', err);
return [];
}
}
// Setup toolbar for note editors
function setupNoteEditorToolbar(editorId, containerId) {
const editor = document.getElementById(editorId);
const container = document.getElementById(containerId);
if(!editor || !container) return;
let savedSelection = null;
function saveSelection() {
const sel = window.getSelection();
if(sel.rangeCount > 0) savedSelection = sel.getRangeAt(0);
}
function restoreSelection() {
const sel = window.getSelection();
if(savedSelection) {
sel.removeAllRanges();
sel.addRange(savedSelection);
}
}
editor.addEventListener('mouseup', saveSelection);
editor.addEventListener('keyup', saveSelection);
editor.addEventListener('focus', saveSelection);
const buttons = container.querySelectorAll('[data-cmd]');
buttons.forEach(btn => {
btn.addEventListener('mousedown', e => {
e.preventDefault();
restoreSelection();
const cmd = btn.dataset.cmd;
if(!cmd) return;
if(cmd === 'foreColor') {
const val = btn.dataset.val;
document.execCommand(cmd, false, val);
buttons.forEach(b => {
if(b.dataset.cmd === 'foreColor' && b !== btn) {
b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600');
}
});
btn.classList.toggle('bg-blue-100');
btn.classList.toggle('text-gray-900');
btn.classList.toggle('border-blue-600');
} else if(cmd === 'removeFormat') {
restoreSelection();
const sel = window.getSelection();
if(sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
let content = range.toString();
range.deleteContents();
range.insertNode(document.createTextNode(content));
range.setStartAfter(range.startContainer);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
buttons.forEach(b => b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600'));
} else {
document.execCommand(cmd, false, null);
btn.classList.toggle('bg-blue-100');
btn.classList.toggle('text-gray-900');
btn.classList.toggle('border-blue-600');
}
editor.focus();
saveSelection();
});
});
}
// Submit note function
async function submitNote(editorId, job, isManagerNote) {
const editor = document.getElementById(editorId);
const html = editor.innerHTML.trim();
if(!html) {
alert('Enter note text');
return;
}
const now = new Date();
const timestamp = now.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
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 || '');
try {
const payloadNote = {
Note: formatted.trim(),
Job_Number: job?.Job_Number || '',
Job_Number_Id: job?.id ? [job.id] : []
};
// 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(!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];
const resNote = await fetch(`${NOTES_COLLECTION_URL}`, {
method: 'POST',
headers: {'Content-Type': 'application/json', ...authHeaders()},
body: JSON.stringify(payloadNote)
});
if(!resNote.ok) {
console.warn('Note creation failed with status', resNote.status);
}
// Update UI
currentJob.Notes = updatedNotes;
editor.innerHTML = '';
alert('Note saved successfully!');
// Refresh the display
if(currentJob) {
populateManagerInfo(currentJob);
}
} catch(err) {
console.error('Error saving note:', err);
alert('Failed to save note: ' + err.message);
}
}
// Function to populate file columns in notes section
function populateNotesFileColumns(files, containerId) {
const container = document.getElementById(containerId);
if(!container) {
console.log('Container not found:', containerId);
return;
}
console.log('Populating', containerId, 'with', files.length, 'files');
container.innerHTML = '';
// Categorize files
const plans = [];
const estimates = [];
const otherFiles = [];
if(files && files.length > 0) {
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 {
otherFiles.push(file);
}
});
}
// 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 = '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);
const fileName = document.createElement('span');
fileName.textContent = file.name;
fileName.className = 'truncate';
fileItem.appendChild(fileName);
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 = '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);
const fileName = document.createElement('span');
fileName.textContent = file.name;
fileName.className = 'truncate';
fileItem.appendChild(fileName);
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);
// Other Files column
const otherCol = document.createElement('div');
otherCol.innerHTML = `<div class="font-bold text-gray-700 uppercase tracking-wide mb-1" style="font-size: 8px;">Other Files</div>`;
const otherList = document.createElement('div');
otherList.className = 'space-y-1';
if(otherFiles.length > 0) {
otherFiles.forEach(file => {
const fileItem = document.createElement('div');
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);
const fileName = document.createElement('span');
fileName.textContent = file.name;
fileName.className = 'truncate';
fileItem.appendChild(fileName);
fileItem.onclick = () => openFilePreview(file);
otherList.appendChild(fileItem);
});
} else {
otherList.innerHTML = `<div class="text-gray-400 italic" style="font-size: 10px;">No other files</div>`;
}
otherCol.appendChild(otherList);
container.appendChild(plansCol);
container.appendChild(estimatesCol);
container.appendChild(otherCol);
}
const closeModal = ()=>{ detailModal.classList.add('hidden'); renderCards(visibleJobs); }; const closeModal = ()=>{ detailModal.classList.add('hidden'); renderCards(visibleJobs); };
document.getElementById('btnClose').addEventListener('click',closeModal); document.getElementById('btnClose').addEventListener('click',closeModal);
document.getElementById('backBtn').addEventListener('click',closeModal); document.getElementById('backBtn').addEventListener('click',closeModal);
@@ -1130,30 +1943,53 @@
function switchTab(tab){ function switchTab(tab){
const infoTab = document.getElementById('infoTab'); const infoTab = document.getElementById('infoTab');
const notesTab = document.getElementById('notesTab'); const notesTab = document.getElementById('notesTab');
const managerInfoTab = document.getElementById('managerInfoTab');
const infoContent = document.getElementById('infoContent'); const infoContent = document.getElementById('infoContent');
const notesContent = document.getElementById('notesContent'); const notesContent = document.getElementById('notesContent');
const managerInfoContent = document.getElementById('managerInfoContent');
const infoHeading = document.getElementById('infoHeading'); const infoHeading = document.getElementById('infoHeading');
const notesHeading = document.getElementById('notesHeading'); const notesHeading = document.getElementById('notesHeading');
const managerInfoHeading = document.getElementById('managerInfoHeading');
if(tab === 'info'){ if(tab === 'info'){
infoTab.className = 'px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px'; infoTab.className = 'px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px';
notesTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700'; notesTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
managerInfoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
infoContent.classList.remove('hidden'); infoContent.classList.remove('hidden');
notesContent.classList.add('hidden'); notesContent.classList.add('hidden');
managerInfoContent.classList.add('hidden');
if(infoHeading) infoHeading.classList.remove('hidden'); if(infoHeading) infoHeading.classList.remove('hidden');
if(notesHeading) notesHeading.classList.add('hidden'); if(notesHeading) notesHeading.classList.add('hidden');
} else { if(managerInfoHeading) managerInfoHeading.classList.add('hidden');
} else if(tab === 'notes') {
infoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700'; infoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
notesTab.className = 'px-4 py-2 text-sm font-medium text-orange-600 border-b-2 border-orange-600 -mb-px'; notesTab.className = 'px-4 py-2 text-sm font-medium text-orange-600 border-b-2 border-orange-600 -mb-px';
managerInfoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
infoContent.classList.add('hidden'); infoContent.classList.add('hidden');
notesContent.classList.remove('hidden'); notesContent.classList.remove('hidden');
managerInfoContent.classList.add('hidden');
if(infoHeading) infoHeading.classList.add('hidden'); if(infoHeading) infoHeading.classList.add('hidden');
if(notesHeading) notesHeading.classList.remove('hidden'); if(notesHeading) notesHeading.classList.remove('hidden');
if(managerInfoHeading) managerInfoHeading.classList.add('hidden');
} else if(tab === 'managerInfo') {
infoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
notesTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
managerInfoTab.className = 'px-4 py-2 text-sm font-medium text-green-600 border-b-2 border-green-600 -mb-px';
infoContent.classList.add('hidden');
notesContent.classList.add('hidden');
managerInfoContent.classList.remove('hidden');
if(infoHeading) infoHeading.classList.add('hidden');
if(notesHeading) notesHeading.classList.add('hidden');
if(managerInfoHeading) managerInfoHeading.classList.remove('hidden');
} }
} }
document.getElementById('infoTab').addEventListener('click',()=>switchTab('info')); document.getElementById('infoTab').addEventListener('click',()=>switchTab('info'));
document.getElementById('notesTab').addEventListener('click',()=>switchTab('notes')); document.getElementById('notesTab').addEventListener('click',()=>switchTab('notes'));
document.getElementById('managerInfoTab').addEventListener('click',()=>{
switchTab('managerInfo');
populateManagerInfo(currentJob);
});
// --- Toolbar --- // --- Toolbar ---
let savedSelection = null; let savedSelection = null;