// 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 = '
`;
}).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 = '
';
// Previous button
if (currentPage > 1) {
html += ``;
}
// Page info
html += `Page ${currentPage} of ${totalPages} (${totalItems} total)`;
// Next button
if (currentPage < totalPages) {
html += ``;
}
html += '
';
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 = '';
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 = `
${message}
`;
// 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));
}