Files
2025-12-15 14:54:29 -06:00

903 lines
35 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="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:shadow-2xl hover:border-white/50 hover:bg-white/70 cursor-pointer transition-all duration-300 hover:scale-[1.02]"
>
<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>
${job.Phone_Number ? `<a href="tel:${job.Phone_Number.replace(/[^\d+]/g, '')}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">${job.Phone_Number}</a>` : ''}
</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>
${job.Job_Address ? `
<div>
<span class="text-xs text-gray-500 uppercase tracking-wide">Address</span>
<p class="text-sm font-medium text-gray-900 mt-1">${job.Job_Address}</p>
<div class="mt-2 flex flex-col gap-2">
<a
href="https://maps.apple.com/?q=${encodeURIComponent(job.Job_Address)}"
target="_blank"
onclick="event.stopPropagation()"
class="w-full px-3 py-1.5 backdrop-blur-sm bg-gray-900/90 text-white text-xs font-medium rounded-xl hover:bg-gray-800/90 transition-all shadow-lg hover:shadow-xl text-center"
>
Apple Maps
</a>
<a
href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(job.Job_Address)}"
target="_blank"
onclick="event.stopPropagation()"
class="w-full px-3 py-1.5 backdrop-blur-sm bg-blue-600/90 text-white text-xs font-medium rounded-xl hover:bg-blue-700/90 transition-all shadow-lg hover:shadow-xl text-center"
>
Google Maps
</a>
</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') {
// Show table, hide card
if (tableView) tableView.classList.remove('hidden');
if (cardView) cardView.classList.add('hidden');
// Table button: active (indigo with shadow)
if (tableViewBtn) {
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
tableViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
}
// Card button: inactive (transparent)
if (cardViewBtn) {
cardViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
cardViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
}
} else {
// Show card, hide table
if (tableView) tableView.classList.add('hidden');
if (cardView) cardView.classList.remove('hidden');
// Table button: inactive (transparent)
if (tableViewBtn) {
tableViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
tableViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
}
// Card button: active (indigo with shadow)
if (cardViewBtn) {
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
cardViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
}
}
}
// 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));
}