INIT
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
// Authentication functions
|
||||
|
||||
/**
|
||||
* Login with email and password
|
||||
*/
|
||||
async function login(email, password) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
return authData;
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
throw new Error(error.message || 'Login failed. Please check your credentials.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout current user
|
||||
*/
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated, redirect to login if not
|
||||
*/
|
||||
function checkAuth() {
|
||||
if (!pb.authStore.isValid) {
|
||||
window.location.href = 'index.html';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
function getCurrentUser() {
|
||||
return pb.authStore.model;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Configuration loader
|
||||
let appConfig = null;
|
||||
|
||||
/**
|
||||
* Load configuration from config.json
|
||||
*/
|
||||
async function loadConfig() {
|
||||
if (appConfig) {
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/config.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load config: ${response.statusText}`);
|
||||
}
|
||||
appConfig = await response.json();
|
||||
return appConfig;
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
// Return default config if file can't be loaded
|
||||
return {
|
||||
pocketbase: {
|
||||
collections: {
|
||||
jobs: "Job_Info",
|
||||
notes: "notes",
|
||||
users: "users"
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
itemsPerPage: 20
|
||||
},
|
||||
filters: {
|
||||
defaultFilter: "all"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the jobs collection name
|
||||
*/
|
||||
async function getJobsCollection() {
|
||||
const config = await loadConfig();
|
||||
return config.pocketbase.collections.jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notes collection name
|
||||
*/
|
||||
async function getNotesCollection() {
|
||||
const config = await loadConfig();
|
||||
return config.pocketbase.collections.notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items per page setting
|
||||
*/
|
||||
async function getItemsPerPage() {
|
||||
const config = await loadConfig();
|
||||
return config.pagination.itemsPerPage || 20;
|
||||
}
|
||||
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
// 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));
|
||||
}
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
// Notes/reports operations
|
||||
|
||||
let notesCollectionName = 'notes'; // Default, will be loaded from config
|
||||
let jobsCollectionName = 'Job_Info'; // Default, will be loaded from config
|
||||
|
||||
/**
|
||||
* Initialize configuration
|
||||
*/
|
||||
async function initNotesConfig() {
|
||||
try {
|
||||
if (typeof getNotesCollection !== 'undefined') {
|
||||
notesCollectionName = await getNotesCollection();
|
||||
}
|
||||
if (typeof getJobsCollection !== 'undefined') {
|
||||
jobsCollectionName = await getJobsCollection();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing notes config:', error);
|
||||
// Use defaults if config fails to load
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize config when script loads
|
||||
if (typeof getNotesCollection !== 'undefined' || typeof getJobsCollection !== 'undefined') {
|
||||
initNotesConfig();
|
||||
}
|
||||
|
||||
// Get note editor instance from global scope or window
|
||||
function getNoteEditor() {
|
||||
return window.noteEditorInstance || noteEditorInstance || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notes for a specific job
|
||||
*/
|
||||
async function loadJobNotes() {
|
||||
try {
|
||||
const jobId = document.getElementById('jobId').value;
|
||||
if (!jobId) {
|
||||
document.getElementById('notesList').innerHTML = '<p class="text-center text-gray-500 py-8">No job selected</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const job = await pb.collection(jobsCollectionName).getOne(jobId);
|
||||
const noteRefs = job.Note_Ref || [];
|
||||
|
||||
if (noteRefs.length === 0) {
|
||||
document.getElementById('notesList').innerHTML = '<p class="text-center text-gray-500 py-8">No notes for this job</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all notes
|
||||
const notes = await Promise.all(
|
||||
noteRefs.map(noteId =>
|
||||
pb.collection(notesCollectionName).getOne(noteId).catch(() => null)
|
||||
)
|
||||
);
|
||||
|
||||
const validNotes = notes.filter(note => note !== null);
|
||||
|
||||
if (validNotes.length === 0) {
|
||||
document.getElementById('notesList').innerHTML = '<p class="text-center text-gray-500 py-8">No notes found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
displayNotes(validNotes, true);
|
||||
} catch (error) {
|
||||
console.error('Error loading job notes:', error);
|
||||
document.getElementById('notesList').innerHTML = '<p class="text-center text-red-500 py-8">Error loading notes</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all notes
|
||||
*/
|
||||
async function loadAllNotes() {
|
||||
try {
|
||||
const result = await pb.collection(notesCollectionName).getList(1, 100, {
|
||||
sort: '-created',
|
||||
expand: 'job'
|
||||
});
|
||||
|
||||
if (result.items.length === 0) {
|
||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||
document.getElementById('emptyState').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||
document.getElementById('emptyState').classList.add('hidden');
|
||||
document.getElementById('reportsTableContainer').classList.remove('hidden');
|
||||
|
||||
displayNotesTable(result.items);
|
||||
} catch (error) {
|
||||
console.error('Error loading notes:', error);
|
||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||
document.getElementById('emptyState').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notes in list format (for job modal)
|
||||
*/
|
||||
function displayNotes(notes, showDelete = false) {
|
||||
const container = document.getElementById('notesList');
|
||||
|
||||
if (!notes || notes.length === 0) {
|
||||
container.innerHTML = '<p class="text-center text-gray-500 py-8">No notes</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = notes.map(note => {
|
||||
const date = note.created ? new Date(note.created).toLocaleString() : '-';
|
||||
const content = note.content || '';
|
||||
const sentiment = note.sentiment || '-';
|
||||
|
||||
return `
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">${sentiment}</div>
|
||||
<div class="text-xs text-gray-500">${date}</div>
|
||||
</div>
|
||||
${showDelete ? `<button onclick="deleteNoteFromJob('${note.id}')" class="text-red-600 hover:text-red-700 text-sm">Delete</button>` : ''}
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 mt-2">${content}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notes in table format (for notes page)
|
||||
*/
|
||||
function displayNotesTable(notes) {
|
||||
const tbody = document.getElementById('notesTableBody');
|
||||
|
||||
tbody.innerHTML = notes.map(note => {
|
||||
const date = note.created ? new Date(note.created).toLocaleString() : '-';
|
||||
const content = note.content ? note.content.substring(0, 100) + '...' : '-';
|
||||
const jobName = note.job ? (note.job.Job_Number || note.job.Job_Name || '-') : '-';
|
||||
const sentiment = note.sentiment || '-';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${jobName}</td>
|
||||
<td class="px-3 sm:px-6 py-4 text-sm text-gray-900">${content}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${sentiment}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${date}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button onclick="openNoteModal('${note.id}')" class="text-indigo-600 hover:text-indigo-700 mr-4">Edit</button>
|
||||
<button onclick="deleteNote('${note.id}')" class="text-red-600 hover:text-red-700">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load note for editing
|
||||
*/
|
||||
async function loadNoteForEdit(noteId) {
|
||||
try {
|
||||
const note = await pb.collection(notesCollectionName).getOne(noteId);
|
||||
|
||||
document.getElementById('noteId').value = note.id;
|
||||
document.getElementById('noteSentiment').value = note.sentiment || '';
|
||||
document.getElementById('noteDate').value = note.date ? new Date(note.date).toISOString().slice(0, 16) : '';
|
||||
|
||||
if (note.job) {
|
||||
document.getElementById('noteJob').value = note.job;
|
||||
}
|
||||
|
||||
// Set Quill editor content
|
||||
const editor = getNoteEditor();
|
||||
if (editor && note.content) {
|
||||
editor.root.innerHTML = note.content;
|
||||
}
|
||||
|
||||
// Load jobs for select if not already loaded
|
||||
await loadJobsForSelect();
|
||||
} catch (error) {
|
||||
console.error('Error loading note:', error);
|
||||
showMessage('Error loading note: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save note (create or update)
|
||||
*/
|
||||
async function saveNote() {
|
||||
try {
|
||||
const noteId = document.getElementById('noteId')?.value;
|
||||
const jobId = document.getElementById('noteJob')?.value || document.getElementById('jobId')?.value;
|
||||
const editor = getNoteEditor();
|
||||
const content = document.getElementById('noteContent')?.value || editor?.root.innerHTML || '';
|
||||
const sentiment = document.getElementById('noteSentiment')?.value || '';
|
||||
const date = document.getElementById('noteDate')?.value || new Date().toISOString();
|
||||
|
||||
if (!jobId && !noteId) {
|
||||
showMessage('Please select a job', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const noteData = {
|
||||
content: content,
|
||||
sentiment: sentiment,
|
||||
date: date
|
||||
};
|
||||
|
||||
let savedNote;
|
||||
|
||||
if (noteId) {
|
||||
// Update existing note
|
||||
savedNote = await pb.collection(notesCollectionName).update(noteId, noteData);
|
||||
showMessage('Note updated successfully', 'success');
|
||||
} else {
|
||||
// Create new note
|
||||
savedNote = await pb.collection(notesCollectionName).create(noteData);
|
||||
showMessage('Note created successfully', 'success');
|
||||
|
||||
// Link note to job
|
||||
if (jobId) {
|
||||
await linkNoteToJob(savedNote.id, jobId);
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal and reload
|
||||
if (document.getElementById('noteModal')) {
|
||||
closeNoteModal();
|
||||
loadAllNotes();
|
||||
} else {
|
||||
// In job modal
|
||||
cancelAddNote();
|
||||
loadJobNotes();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving note:', error);
|
||||
showMessage('Error saving note: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link note to job
|
||||
*/
|
||||
async function linkNoteToJob(noteId, jobId) {
|
||||
try {
|
||||
const job = await pb.collection(jobsCollectionName).getOne(jobId);
|
||||
const noteRefs = job.Note_Ref || [];
|
||||
|
||||
if (!noteRefs.includes(noteId)) {
|
||||
noteRefs.push(noteId);
|
||||
await pb.collection(jobsCollectionName).update(jobId, {
|
||||
Note_Ref: noteRefs
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error linking note to job:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete note
|
||||
*/
|
||||
async function deleteNote(noteId) {
|
||||
if (!confirm('Are you sure you want to delete this note?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await pb.collection(notesCollectionName).delete(noteId);
|
||||
showMessage('Note deleted successfully', 'success');
|
||||
loadAllNotes();
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error);
|
||||
showMessage('Error deleting note: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete note from job (unlink)
|
||||
*/
|
||||
async function deleteNoteFromJob(noteId) {
|
||||
if (!confirm('Are you sure you want to delete this note?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const jobId = document.getElementById('jobId').value;
|
||||
if (jobId) {
|
||||
const job = await pb.collection(jobsCollectionName).getOne(jobId);
|
||||
const noteRefs = (job.Note_Ref || []).filter(id => id !== noteId);
|
||||
await pb.collection(jobsCollectionName).update(jobId, {
|
||||
Note_Ref: noteRefs
|
||||
});
|
||||
}
|
||||
|
||||
await pb.collection(notesCollectionName).delete(noteId);
|
||||
showMessage('Note deleted successfully', 'success');
|
||||
loadJobNotes();
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error);
|
||||
showMessage('Error deleting note: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show add note form (in job modal)
|
||||
*/
|
||||
function showAddNoteForm() {
|
||||
document.getElementById('addNoteForm').classList.remove('hidden');
|
||||
const editor = getNoteEditor();
|
||||
if (editor) {
|
||||
editor.setContents([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel add note form
|
||||
*/
|
||||
function cancelAddNote() {
|
||||
document.getElementById('addNoteForm').classList.add('hidden');
|
||||
const editor = getNoteEditor();
|
||||
if (editor) {
|
||||
editor.setContents([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// PocketBase client initialization
|
||||
// Update this URL to match your PocketBase instance
|
||||
const POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
||||
|
||||
// Initialize PocketBase client
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
|
||||
// Handle connection errors
|
||||
pb.beforeSend = function (url, options) {
|
||||
console.log('PocketBase request:', url, options);
|
||||
return { url, options };
|
||||
};
|
||||
|
||||
// Handle auth state changes
|
||||
pb.authStore.onChange((token, model) => {
|
||||
console.log('Auth state changed:', token ? 'authenticated' : 'not authenticated');
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = pb;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user