869 lines
33 KiB
JavaScript
869 lines
33 KiB
JavaScript
// Job CRUD operations
|
|
|
|
let currentPage = 1;
|
|
let itemsPerPage = 20;
|
|
let allJobs = [];
|
|
let jobsCollectionName = 'Job_Info'; // Default, will be loaded from config
|
|
let configInitialized = false;
|
|
let configInitializing = false;
|
|
let currentView = localStorage.getItem('jobsView') || 'table'; // 'table' or 'card'
|
|
|
|
/**
|
|
* Load all jobs with optional filters
|
|
*/
|
|
async function loadJobs(page = 1) {
|
|
try {
|
|
// Show loading state
|
|
// Show loading state based on current view
|
|
if (currentView === 'table') {
|
|
const tbody = document.getElementById('jobsTableBody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr id="loadingRow"><td colspan="8" class="px-6 py-8 text-center text-gray-500">Loading...</td></tr>';
|
|
}
|
|
} else {
|
|
const container = document.getElementById('jobsCardContainer');
|
|
if (container) {
|
|
container.innerHTML = '<div class="col-span-full text-center py-8 text-gray-500">Loading...</div>';
|
|
}
|
|
}
|
|
|
|
currentPage = page;
|
|
const searchTerm = document.getElementById('searchInput')?.value || '';
|
|
const estimatorFilter = window.currentEstimatorFilter || 'all';
|
|
const statusFilter = window.currentStatusFilter || 'all';
|
|
|
|
// Build estimator filter string
|
|
let estimatorFilterString = '';
|
|
if (estimatorFilter && estimatorFilter !== 'all') {
|
|
// Escape special characters for PocketBase filter
|
|
const escapedFilter = estimatorFilter.replace(/"/g, '\\"');
|
|
estimatorFilterString = `Estimator = "${escapedFilter}"`;
|
|
}
|
|
|
|
// Build status filter string
|
|
let statusFilterString = '';
|
|
if (statusFilter && statusFilter !== 'all') {
|
|
// Escape special characters for PocketBase filter
|
|
const escapedStatus = statusFilter.replace(/"/g, '\\"');
|
|
statusFilterString = `Job_Status = "${escapedStatus}"`;
|
|
}
|
|
|
|
// Build search filter
|
|
let searchFilter = '';
|
|
if (searchTerm) {
|
|
// Escape special characters for PocketBase filter
|
|
const escapedTerm = searchTerm.replace(/"/g, '\\"');
|
|
searchFilter = `Job_Number ~ "${escapedTerm}" || Job_Name ~ "${escapedTerm}" || Company_Client ~ "${escapedTerm}"`;
|
|
}
|
|
|
|
// Combine filters
|
|
const filterParts = [];
|
|
if (estimatorFilterString) filterParts.push(`(${estimatorFilterString})`);
|
|
if (statusFilterString) filterParts.push(`(${statusFilterString})`);
|
|
if (searchFilter) filterParts.push(`(${searchFilter})`);
|
|
|
|
let finalFilter = '';
|
|
if (filterParts.length > 0) {
|
|
finalFilter = filterParts.join(' && ');
|
|
}
|
|
|
|
const result = await pb.collection(jobsCollectionName).getList(page, itemsPerPage, {
|
|
filter: finalFilter || undefined,
|
|
sort: '-Job_Number',
|
|
});
|
|
|
|
allJobs = result.items;
|
|
displayJobs(allJobs);
|
|
displayPagination(result.page, result.totalPages, result.totalItems);
|
|
|
|
// Update view based on current view preference
|
|
updateViewDisplay();
|
|
} catch (error) {
|
|
console.error('Error loading jobs:', error);
|
|
showMessage('Error loading jobs: ' + (error.message || 'Unknown error'), 'error');
|
|
if (currentView === 'table') {
|
|
const tbody = document.getElementById('jobsTableBody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="px-6 py-8 text-center text-red-500">Error loading jobs. Please refresh the page.</td></tr>';
|
|
}
|
|
} else {
|
|
const container = document.getElementById('jobsCardContainer');
|
|
if (container) {
|
|
container.innerHTML = '<div class="col-span-full text-center py-8 text-red-500">Error loading jobs. Please refresh the page.</div>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get estimator badge HTML
|
|
*/
|
|
function getEstimatorBadgeHtml(estimator) {
|
|
if (!estimator) return '-';
|
|
|
|
const estimatorLower = estimator.toLowerCase();
|
|
let bgColor = 'bg-gray-100';
|
|
let textColor = 'text-gray-800';
|
|
|
|
if (estimatorLower.includes('steve')) {
|
|
bgColor = 'bg-orange-100';
|
|
textColor = 'text-orange-800';
|
|
} else if (estimatorLower.includes('beth')) {
|
|
bgColor = 'bg-blue-100';
|
|
textColor = 'text-blue-800';
|
|
} else if (estimatorLower.includes('timothy')) {
|
|
bgColor = 'bg-purple-100';
|
|
textColor = 'text-purple-800';
|
|
}
|
|
|
|
return `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${bgColor} ${textColor}">${estimator}</span>`;
|
|
}
|
|
|
|
/**
|
|
* Get status badge color
|
|
*/
|
|
function getStatusBadgeColor(status) {
|
|
if (!status) return 'bg-gray-200 text-gray-700';
|
|
|
|
const statusLower = status.toLowerCase();
|
|
|
|
if (statusLower.includes('billed') && (statusLower.includes('closed') || statusLower.includes('progress'))) {
|
|
return 'bg-purple-200 text-purple-800';
|
|
} else if (statusLower.includes('est hold')) {
|
|
return 'bg-orange-200 text-orange-800';
|
|
} else if (statusLower.includes('est sent') || statusLower.includes('follow up')) {
|
|
return 'bg-yellow-200 text-yellow-800';
|
|
} else if (statusLower.includes('estimating')) {
|
|
return 'bg-blue-200 text-blue-800';
|
|
} else if (statusLower.includes('in progress') || statusLower.includes('awarded')) {
|
|
return 'bg-green-200 text-green-800';
|
|
} else if (statusLower.includes('not awarded') || statusLower.includes('not bidding')) {
|
|
return 'bg-red-200 text-red-800';
|
|
} else if (statusLower.includes('archive')) {
|
|
return 'bg-gray-200 text-gray-700';
|
|
}
|
|
|
|
return 'bg-gray-200 text-gray-700';
|
|
}
|
|
|
|
/**
|
|
* Display jobs in table or card view
|
|
*/
|
|
function displayJobs(jobs) {
|
|
if (currentView === 'table') {
|
|
displayJobsTable(jobs);
|
|
} else {
|
|
displayJobsCards(jobs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display jobs in table view
|
|
*/
|
|
function displayJobsTable(jobs) {
|
|
const tbody = document.getElementById('jobsTableBody');
|
|
if (!tbody) return;
|
|
|
|
if (!jobs || jobs.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="px-6 py-8 text-center text-gray-500">No jobs found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = jobs.map(job => {
|
|
const estimatorDisplay = getEstimatorBadgeHtml(job.Estimator);
|
|
|
|
return `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Job_Number || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Job_Name || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Company_Client || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm">${estimatorDisplay}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Job_Status || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Job_Type || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.Job_Division || '-'}</td>
|
|
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button onclick="openJobModal('${job.id}')" class="bg-green-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition">Open</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Display jobs in card view (multiple lines, single column)
|
|
*/
|
|
function displayJobsCards(jobs) {
|
|
const container = document.getElementById('jobsCardContainer');
|
|
if (!container) return;
|
|
|
|
if (!jobs || jobs.length === 0) {
|
|
container.innerHTML = '<div class="text-center py-8 text-gray-500">No jobs found</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = jobs.map(job => {
|
|
const estimatorBadge = getEstimatorBadgeHtml(job.Estimator);
|
|
const statusColor = getStatusBadgeColor(job.Job_Status);
|
|
|
|
return `
|
|
<div
|
|
onclick="openJobModal('${job.id}')"
|
|
class="bg-white rounded-lg shadow border border-gray-200 p-6 hover:shadow-lg hover:border-indigo-300 cursor-pointer transition-all duration-200 hover:bg-indigo-50/30"
|
|
>
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<h3 class="text-xl font-semibold text-gray-900">${job.Job_Number || 'N/A'}</h3>
|
|
<p class="text-base text-gray-600 mt-1">${job.Job_Name || '-'}</p>
|
|
</div>
|
|
<svg class="w-5 h-5 text-gray-400 flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Client</span>
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${job.Company_Client || '-'}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Estimator</span>
|
|
<div class="mt-1">${estimatorBadge}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Status</span>
|
|
<div class="mt-1">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
|
|
${job.Job_Status || '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 sm:col-span-2 lg:col-span-1">
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Type</span>
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${job.Job_Type || '-'}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">Division</span>
|
|
<p class="text-sm font-medium text-gray-900 mt-1">${job.Job_Division || '-'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Switch between table and card view
|
|
*/
|
|
function switchView(view) {
|
|
currentView = view;
|
|
localStorage.setItem('jobsView', view);
|
|
updateViewDisplay();
|
|
displayJobs(allJobs);
|
|
}
|
|
|
|
/**
|
|
* Update view display based on current view preference
|
|
*/
|
|
function updateViewDisplay() {
|
|
const tableView = document.getElementById('tableView');
|
|
const cardView = document.getElementById('cardView');
|
|
const tableViewBtn = document.getElementById('tableViewBtn');
|
|
const cardViewBtn = document.getElementById('cardViewBtn');
|
|
|
|
if (currentView === 'table') {
|
|
if (tableView) tableView.classList.remove('hidden');
|
|
if (cardView) cardView.classList.add('hidden');
|
|
if (tableViewBtn) {
|
|
tableViewBtn.classList.add('bg-indigo-600', 'text-white');
|
|
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
|
|
}
|
|
if (cardViewBtn) {
|
|
cardViewBtn.classList.remove('bg-indigo-600', 'text-white');
|
|
cardViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
|
|
}
|
|
} else {
|
|
if (tableView) tableView.classList.add('hidden');
|
|
if (cardView) cardView.classList.remove('hidden');
|
|
if (tableViewBtn) {
|
|
tableViewBtn.classList.remove('bg-indigo-600', 'text-white');
|
|
tableViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
|
|
}
|
|
if (cardViewBtn) {
|
|
cardViewBtn.classList.add('bg-indigo-600', 'text-white');
|
|
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make functions globally accessible
|
|
window.switchView = switchView;
|
|
window.updateViewDisplay = updateViewDisplay;
|
|
|
|
/**
|
|
* Display pagination controls
|
|
*/
|
|
function displayPagination(currentPage, totalPages, totalItems) {
|
|
const container = document.getElementById('paginationContainer');
|
|
if (!container) return;
|
|
|
|
if (totalPages <= 1) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="flex items-center gap-2">';
|
|
|
|
// Previous button
|
|
if (currentPage > 1) {
|
|
html += `<button onclick="loadJobs(${currentPage - 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Previous</button>`;
|
|
}
|
|
|
|
// Page info
|
|
html += `<span class="text-sm text-gray-700">Page ${currentPage} of ${totalPages} (${totalItems} total)</span>`;
|
|
|
|
// Next button
|
|
if (currentPage < totalPages) {
|
|
html += `<button onclick="loadJobs(${currentPage + 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Next</button>`;
|
|
}
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Load job for editing
|
|
*/
|
|
async function loadJobForEdit(jobId) {
|
|
try {
|
|
if (!jobId) {
|
|
showMessage('No job ID provided', 'error');
|
|
return null;
|
|
}
|
|
|
|
const job = await pb.collection(jobsCollectionName).getOne(jobId);
|
|
|
|
// Update dashboard header and quick info
|
|
if (typeof updateDashboardInfo === 'function') {
|
|
updateDashboardInfo(job);
|
|
}
|
|
|
|
// Populate form fields
|
|
Object.keys(job).forEach(key => {
|
|
const field = document.getElementById(key);
|
|
if (field) {
|
|
if (field.type === 'checkbox') {
|
|
field.checked = job[key] === true;
|
|
} else if (field.type === 'date') {
|
|
if (job[key]) {
|
|
try {
|
|
const date = new Date(job[key]);
|
|
if (!isNaN(date.getTime())) {
|
|
field.value = date.toISOString().split('T')[0];
|
|
}
|
|
} catch (e) {
|
|
console.warn('Invalid date for field', key, job[key]);
|
|
}
|
|
}
|
|
} else if (field.type === 'time') {
|
|
if (job[key]) {
|
|
field.value = job[key];
|
|
}
|
|
} else {
|
|
field.value = job[key] || '';
|
|
}
|
|
}
|
|
});
|
|
|
|
return job;
|
|
} catch (error) {
|
|
console.error('Error loading job:', error);
|
|
showMessage('Error loading job: ' + (error.message || 'Unknown error'), 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save job (create or update)
|
|
*/
|
|
async function saveJob() {
|
|
try {
|
|
const form = document.getElementById('jobForm');
|
|
if (!form) {
|
|
showMessage('Form not found', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(form);
|
|
const jobId = document.getElementById('jobId')?.value;
|
|
|
|
// Build job object
|
|
const jobData = {};
|
|
const booleanFields = ['Active', 'Flag', 'Tax_Exempt', 'Docs_Uploaded', 'Tax_Exempt_Docs_Uploaded',
|
|
'Estimate_Approved', 'Estimate_Reviewed', 'Estimate_Draft', 'QB_Created',
|
|
'Added_To_Calendar', 'PSwift_Uploaded', 'Voxer_Created'];
|
|
|
|
// Get all form fields
|
|
for (const [key, value] of formData.entries()) {
|
|
if (key === 'id') continue;
|
|
|
|
if (booleanFields.includes(key)) {
|
|
const checkbox = document.getElementById(key);
|
|
jobData[key] = checkbox ? checkbox.checked : false;
|
|
} else if (value && value.trim() !== '') {
|
|
jobData[key] = value.trim();
|
|
}
|
|
}
|
|
|
|
if (jobId) {
|
|
// Update existing job
|
|
await pb.collection(jobsCollectionName).update(jobId, jobData);
|
|
showMessage('Job updated successfully', 'success');
|
|
} else {
|
|
// Create new job
|
|
await pb.collection(jobsCollectionName).create(jobData);
|
|
showMessage('Job created successfully', 'success');
|
|
}
|
|
|
|
if (typeof closeJobModal === 'function') {
|
|
closeJobModal();
|
|
}
|
|
loadJobs(currentPage);
|
|
} catch (error) {
|
|
console.error('Error saving job:', error);
|
|
const errorMsg = error.message || 'Unknown error';
|
|
showMessage('Error saving job: ' + errorMsg, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete job
|
|
*/
|
|
async function deleteJob(jobId) {
|
|
if (!confirm('Are you sure you want to delete this job?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await pb.collection(jobsCollectionName).delete(jobId);
|
|
showMessage('Job deleted successfully', 'success');
|
|
loadJobs(currentPage);
|
|
} catch (error) {
|
|
console.error('Error deleting job:', error);
|
|
showMessage('Error deleting job: ' + (error.message || 'Unknown error'), 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get job statistics for dashboard
|
|
*/
|
|
async function getJobStats() {
|
|
try {
|
|
// Ensure config is loaded before getting stats (only once)
|
|
if (typeof initConfig !== 'undefined') {
|
|
await initConfig();
|
|
}
|
|
|
|
const collectionName = jobsCollectionName || 'Job_Info'; // Fallback to default
|
|
|
|
// Make requests sequentially to avoid auto-cancellation
|
|
const totalResult = await pb.collection(collectionName).getList(1, 1);
|
|
const activeResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Active = true'
|
|
});
|
|
const estimatingResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Estimating"'
|
|
});
|
|
const estSentResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Est Sent"'
|
|
});
|
|
const estHoldResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Est Hold"'
|
|
});
|
|
const followUpResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Follow Up"'
|
|
});
|
|
const awardedResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Awarded"'
|
|
});
|
|
const inProgressResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "In Progress"'
|
|
});
|
|
const billedInProgressResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Billed (In Progress)"'
|
|
});
|
|
const billedClosedResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Billed (Closed)"'
|
|
});
|
|
const notAwardedResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Not Awarded"'
|
|
});
|
|
const notBiddingResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Not Bidding"'
|
|
});
|
|
const archiveResult = await pb.collection(collectionName).getList(1, 1, {
|
|
filter: 'Job_Status = "Archive"'
|
|
});
|
|
|
|
const stats = {
|
|
total: totalResult.totalItems || 0,
|
|
active: activeResult.totalItems || 0,
|
|
estimating: estimatingResult.totalItems || 0,
|
|
estSent: estSentResult.totalItems || 0,
|
|
estHold: estHoldResult.totalItems || 0,
|
|
followUp: followUpResult.totalItems || 0,
|
|
awarded: awardedResult.totalItems || 0,
|
|
inProgress: inProgressResult.totalItems || 0,
|
|
billedInProgress: billedInProgressResult.totalItems || 0,
|
|
billedClosed: billedClosedResult.totalItems || 0,
|
|
notAwarded: notAwardedResult.totalItems || 0,
|
|
notBidding: notBiddingResult.totalItems || 0,
|
|
archive: archiveResult.totalItems || 0
|
|
};
|
|
|
|
return stats;
|
|
} catch (error) {
|
|
console.error('Error getting stats:', error);
|
|
return {
|
|
total: 0,
|
|
active: 0,
|
|
estimating: 0,
|
|
estSent: 0,
|
|
estHold: 0,
|
|
followUp: 0,
|
|
awarded: 0,
|
|
inProgress: 0,
|
|
billedInProgress: 0,
|
|
billedClosed: 0,
|
|
notAwarded: 0,
|
|
notBidding: 0,
|
|
archive: 0
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load jobs for select dropdown
|
|
*/
|
|
async function loadJobsForSelect() {
|
|
try {
|
|
const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
|
|
sort: 'Job_Number'
|
|
});
|
|
|
|
const select = document.getElementById('noteJob');
|
|
if (!select) return;
|
|
|
|
select.innerHTML = '<option value="">Select a job...</option>';
|
|
result.items.forEach(job => {
|
|
const option = document.createElement('option');
|
|
option.value = job.id;
|
|
option.textContent = `${job.Job_Number || job.id} - ${job.Job_Name || 'Unnamed'}`;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading jobs for select:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load unique estimators and create filter buttons
|
|
*/
|
|
async function loadEstimatorFilters() {
|
|
try {
|
|
// Ensure config is loaded
|
|
await initConfig();
|
|
|
|
// Get all jobs to extract unique estimators
|
|
const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
|
|
fields: 'Estimator'
|
|
});
|
|
|
|
// Extract unique estimators
|
|
const estimators = new Set();
|
|
result.items.forEach(job => {
|
|
if (job.Estimator && job.Estimator.trim() !== '') {
|
|
estimators.add(job.Estimator.trim());
|
|
}
|
|
});
|
|
|
|
// Sort estimators alphabetically
|
|
const sortedEstimators = Array.from(estimators).sort();
|
|
|
|
// Get filter buttons container
|
|
const container = document.getElementById('filterButtons');
|
|
if (!container) return;
|
|
|
|
// Keep the "All" button and add estimator buttons
|
|
const allButton = container.querySelector('#filterAll');
|
|
container.innerHTML = '';
|
|
if (allButton) {
|
|
container.appendChild(allButton);
|
|
}
|
|
|
|
// Add estimator filter buttons with pastel colors
|
|
sortedEstimators.forEach(estimator => {
|
|
const button = document.createElement('button');
|
|
button.onclick = () => {
|
|
if (typeof window.setEstimatorFilter === 'function') {
|
|
window.setEstimatorFilter(estimator);
|
|
}
|
|
};
|
|
button.id = `filter-${estimator.replace(/\s+/g, '-')}`;
|
|
|
|
// Set pastel colors for specific estimators
|
|
let bgColor = 'bg-gray-200';
|
|
let textColor = 'text-gray-700';
|
|
let hoverColor = 'hover:bg-gray-300';
|
|
|
|
const estimatorLower = estimator.toLowerCase();
|
|
if (estimatorLower.includes('steve')) {
|
|
bgColor = 'bg-orange-200';
|
|
textColor = 'text-orange-800';
|
|
hoverColor = 'hover:bg-orange-300';
|
|
} else if (estimatorLower.includes('beth')) {
|
|
bgColor = 'bg-blue-200';
|
|
textColor = 'text-blue-800';
|
|
hoverColor = 'hover:bg-blue-300';
|
|
} else if (estimatorLower.includes('timothy')) {
|
|
bgColor = 'bg-purple-200';
|
|
textColor = 'text-purple-800';
|
|
hoverColor = 'hover:bg-purple-300';
|
|
}
|
|
|
|
button.className = `flex-1 sm:flex-none px-3 py-1.5 rounded-md text-xs font-medium transition focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 ${bgColor} ${textColor} ${hoverColor}`;
|
|
button.textContent = estimator;
|
|
container.appendChild(button);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading estimator filters:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load unique job statuses and create filter buttons
|
|
*/
|
|
async function loadStatusFilters() {
|
|
try {
|
|
// Ensure config is loaded
|
|
await initConfig();
|
|
|
|
// Get all jobs to extract unique statuses
|
|
const result = await pb.collection(jobsCollectionName).getList(1, 1000, {
|
|
fields: 'Job_Status'
|
|
});
|
|
|
|
// Extract unique statuses
|
|
const statuses = new Set();
|
|
result.items.forEach(job => {
|
|
if (job.Job_Status && job.Job_Status.trim() !== '') {
|
|
statuses.add(job.Job_Status.trim());
|
|
}
|
|
});
|
|
|
|
// Define the desired order
|
|
const statusOrder = [
|
|
'Estimating',
|
|
'Est Sent',
|
|
'Est Hold',
|
|
'Follow Up',
|
|
'Awarded',
|
|
'In Progress',
|
|
'Billed (In Progress)',
|
|
'Billed (Closed)',
|
|
'Not Awarded',
|
|
'Not Bidding',
|
|
'Archive'
|
|
];
|
|
|
|
// Sort statuses by the defined order, then alphabetically for any not in the list
|
|
const sortedStatuses = Array.from(statuses).sort((a, b) => {
|
|
const indexA = statusOrder.findIndex(order =>
|
|
a.toLowerCase() === order.toLowerCase() ||
|
|
a.toLowerCase().includes(order.toLowerCase()) ||
|
|
order.toLowerCase().includes(a.toLowerCase())
|
|
);
|
|
const indexB = statusOrder.findIndex(order =>
|
|
b.toLowerCase() === order.toLowerCase() ||
|
|
b.toLowerCase().includes(order.toLowerCase()) ||
|
|
order.toLowerCase().includes(b.toLowerCase())
|
|
);
|
|
|
|
// If both are in the order list, sort by their position
|
|
if (indexA !== -1 && indexB !== -1) {
|
|
return indexA - indexB;
|
|
}
|
|
// If only A is in the list, A comes first
|
|
if (indexA !== -1) return -1;
|
|
// If only B is in the list, B comes first
|
|
if (indexB !== -1) return 1;
|
|
// If neither is in the list, sort alphabetically
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
// Get status filter buttons container
|
|
const container = document.getElementById('statusFilterButtons');
|
|
if (!container) return;
|
|
|
|
// Keep the "All" button and add status buttons
|
|
const allButton = container.querySelector('#statusFilterAll');
|
|
container.innerHTML = '';
|
|
if (allButton) {
|
|
container.appendChild(allButton);
|
|
}
|
|
|
|
// Create buttons for each status with specific colors
|
|
sortedStatuses.forEach(status => {
|
|
const button = document.createElement('button');
|
|
button.textContent = status;
|
|
button.onclick = () => {
|
|
if (typeof window.setStatusFilter === 'function') {
|
|
window.setStatusFilter(status);
|
|
}
|
|
};
|
|
|
|
// Set colors based on status
|
|
let bgColor = 'bg-gray-200';
|
|
let textColor = 'text-gray-700';
|
|
let hoverColor = 'hover:bg-gray-300';
|
|
|
|
const statusLower = status.toLowerCase();
|
|
|
|
// Purple for Billed statuses
|
|
if (statusLower.includes('billed') && statusLower.includes('closed')) {
|
|
bgColor = 'bg-purple-200';
|
|
textColor = 'text-purple-800';
|
|
hoverColor = 'hover:bg-purple-300';
|
|
} else if (statusLower.includes('billed') && statusLower.includes('progress')) {
|
|
bgColor = 'bg-purple-200';
|
|
textColor = 'text-purple-800';
|
|
hoverColor = 'hover:bg-purple-300';
|
|
}
|
|
// Orange for Est Hold
|
|
else if (statusLower.includes('est hold') || statusLower === 'est hold') {
|
|
bgColor = 'bg-orange-200';
|
|
textColor = 'text-orange-800';
|
|
hoverColor = 'hover:bg-orange-300';
|
|
}
|
|
// Yellow for Est Sent and Follow Up
|
|
else if (statusLower.includes('est sent') || statusLower === 'est sent' ||
|
|
statusLower.includes('follow up') || statusLower === 'follow up') {
|
|
bgColor = 'bg-yellow-200';
|
|
textColor = 'text-yellow-800';
|
|
hoverColor = 'hover:bg-yellow-300';
|
|
}
|
|
// Blue for Estimating
|
|
else if (statusLower.includes('estimating') || statusLower === 'estimating') {
|
|
bgColor = 'bg-blue-200';
|
|
textColor = 'text-blue-800';
|
|
hoverColor = 'hover:bg-blue-300';
|
|
}
|
|
// Green for In Progress and Awarded
|
|
else if (statusLower.includes('in progress') || statusLower === 'in progress' ||
|
|
statusLower.includes('awarded') || statusLower === 'awarded') {
|
|
bgColor = 'bg-green-200';
|
|
textColor = 'text-green-800';
|
|
hoverColor = 'hover:bg-green-300';
|
|
}
|
|
// Red for Not Awarded and Not Bidding
|
|
else if (statusLower.includes('not awarded') || statusLower === 'not awarded' ||
|
|
statusLower.includes('not bidding') || statusLower === 'not bidding') {
|
|
bgColor = 'bg-red-200';
|
|
textColor = 'text-red-800';
|
|
hoverColor = 'hover:bg-red-300';
|
|
}
|
|
// Gray for Archive
|
|
else if (statusLower.includes('archive') || statusLower === 'archive') {
|
|
bgColor = 'bg-gray-200';
|
|
textColor = 'text-gray-700';
|
|
hoverColor = 'hover:bg-gray-300';
|
|
}
|
|
|
|
button.className = `flex-1 sm:flex-none px-3 py-1.5 rounded-md text-xs font-medium transition focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 ${bgColor} ${textColor} ${hoverColor}`;
|
|
container.appendChild(button);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading status filters:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show message to user
|
|
*/
|
|
function showMessage(message, type = 'success') {
|
|
const container = document.getElementById('messageContainer');
|
|
if (!container) return;
|
|
|
|
const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700';
|
|
|
|
container.innerHTML = `
|
|
<div class="${bgColor} border px-4 py-3 rounded-lg text-sm">
|
|
${message}
|
|
</div>
|
|
`;
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
container.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Initialize configuration (idempotent - only runs once)
|
|
*/
|
|
async function initConfig() {
|
|
// If already initialized, return immediately
|
|
if (configInitialized) {
|
|
return;
|
|
}
|
|
|
|
// If currently initializing, wait for it to complete
|
|
if (configInitializing) {
|
|
while (configInitializing) {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Mark as initializing
|
|
configInitializing = true;
|
|
|
|
try {
|
|
if (typeof getJobsCollection === 'undefined') {
|
|
console.warn('getJobsCollection not available, using default collection name');
|
|
jobsCollectionName = 'Job_Info';
|
|
configInitialized = true;
|
|
configInitializing = false;
|
|
return;
|
|
}
|
|
const collectionName = await getJobsCollection();
|
|
jobsCollectionName = collectionName;
|
|
console.log('Config initialized, jobs collection:', jobsCollectionName);
|
|
if (typeof getItemsPerPage !== 'undefined') {
|
|
const itemsPerPageConfig = await getItemsPerPage();
|
|
itemsPerPage = itemsPerPageConfig;
|
|
}
|
|
configInitialized = true;
|
|
} catch (error) {
|
|
console.error('Error initializing config:', error);
|
|
// Use defaults if config fails to load
|
|
jobsCollectionName = jobsCollectionName || 'Job_Info';
|
|
configInitialized = true;
|
|
} finally {
|
|
configInitializing = false;
|
|
}
|
|
}
|
|
|
|
// Initialize config when script loads (but don't block)
|
|
// Note: This will be called, but subsequent calls to initConfig() will be idempotent
|
|
if (typeof getJobsCollection !== 'undefined') {
|
|
initConfig().catch(err => console.error('Config initialization error:', err));
|
|
}
|
|
|