updated but still issues with Jobs Page api loading

This commit is contained in:
2025-11-26 12:57:28 -06:00
parent 9eb8be207d
commit c783836bca
+784 -59
View File
@@ -180,7 +180,7 @@
.job-name {
font-weight: 600;
color: var(--gruvbox-fg0;
color: var(--gruvbox-fg0);
}
.job-status {
@@ -333,6 +333,93 @@
font-size: 0.9rem;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(40, 40, 40, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background: var(--gruvbox-bg1);
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
border: 1px solid var(--gruvbox-bg3);
}
.popup-content {
max-width: 400px;
width: 95%;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--gruvbox-bg3);
}
.modal-header h2 {
color: var(--gruvbox-orange);
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
color: var(--gruvbox-fg2);
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.modal-close:hover {
background: var(--gruvbox-bg3);
color: var(--gruvbox-fg0);
}
#job-form {
padding: 1.5rem;
}
#job-form .form-group {
margin-bottom: 1.25rem;
}
#job-form .form-group:last-child {
margin-bottom: 0;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding: 1.5rem;
border-top: 1px solid var(--gruvbox-bg3);
margin-top: 1.5rem;
}
@media (max-width: 768px) {
.main-content {
padding: 1rem;
@@ -390,13 +477,6 @@
<h2 class="table-title">All Jobs</h2>
<div class="search-filter">
<input type="text" id="search-input" class="search-input" placeholder="Search jobs..." onkeyup="filterJobs()">
<select id="status-filter" class="filter-select" onchange="filterJobs()">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
<option value="paused">Paused</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
@@ -412,15 +492,8 @@
<div id="jobs-content" style="display: none;">
<table class="jobs-table" id="jobs-table">
<thead>
<tr>
<th>Job Name</th>
<th>Client</th>
<th>Status</th>
<th>Budget</th>
<th>Start Date</th>
<th>End Date</th>
<th>Last Modified</th>
<th>Actions</th>
<tr id="table-headers">
<!-- Headers will be generated dynamically -->
</tr>
</thead>
<tbody id="jobs-tbody">
@@ -449,11 +522,15 @@
// Initialize PocketBase
const pb = new PocketBase('https://pocketbase.ccllc.pro');
// API key for admin operations (fetched from 'api' collection)
let API_KEY = null;
// State management
let allJobs = [];
let filteredJobs = [];
let currentPage = 1;
const itemsPerPage = 10;
let collectionSchema = null;
// Initialize page
async function initJobsPage() {
@@ -470,7 +547,19 @@
const user = pb.authStore.model;
menuBar.updateUserName(user.name || user.email);
// Load jobs
// Debug: Check authentication and user details
console.log('Current user:', user);
console.log('Auth store:', pb.authStore);
console.log('Is authenticated:', pb.authStore.isValid);
// Debug: Check what collections are available
await debugCollections();
// Fetch API key from 'api' collection for admin operations
await loadApiKey();
// Load collection schema first, then jobs
await loadCollectionSchema();
await loadJobs();
} catch (error) {
@@ -482,24 +571,276 @@
}
}
/**
* Fetch API key from the 'api' collection
* The key is stored in a field called 'API_KEY'
*/
async function loadApiKey() {
// Try different collection name variations
const collectionNames = ['api', 'API', 'Api'];
for (const collectionName of collectionNames) {
try {
console.log(`Trying to load API key from collection: ${collectionName}`);
// Fetch the API key from the collection
const records = await pb.collection(collectionName).getFullList({
sort: '-created'
});
console.log(`Found ${records?.length || 0} record(s) in ${collectionName} collection`);
if (records && records.length > 0) {
const firstRecord = records[0];
console.log(`First record fields:`, Object.keys(firstRecord));
console.log(`First record data:`, firstRecord);
// Try different field name variations
// Priority: API_KEY (as shown in PocketBase admin), then other variations
API_KEY = firstRecord.API_KEY ||
firstRecord.Key ||
firstRecord.key ||
firstRecord.KEY ||
firstRecord.api_key ||
firstRecord.apiKey ||
firstRecord.ApiKey ||
null;
if (API_KEY) {
console.log(`✅ API key loaded successfully from ${collectionName} collection`);
return; // Success, exit function
} else {
console.warn(`API key field not found in ${collectionName} collection record. Available fields:`, Object.keys(firstRecord));
}
} else {
console.warn(`No records found in ${collectionName} collection`);
}
} catch (error) {
console.warn(`Could not load from ${collectionName} collection:`, error.message);
// Try next collection name
}
}
// If we get here, no API key was found
console.warn('⚠️ API key not found in any api collection. Operations will use user authentication.');
}
async function addressToCoordinates(address) {
// Check if input is already coordinates (format: "lat,lng" or "lat, lng")
const coordPattern = /^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$/;
const match = address.trim().match(coordPattern);
if (match) {
// Already coordinates, parse and return as object
const lat = parseFloat(match[1]);
const lon = parseFloat(match[2]);
return { lat, lon };
}
// Try to geocode the address using a geocoding service
// Using OpenStreetMap Nominatim (free, no API key required)
try {
const encodedAddress = encodeURIComponent(address);
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodedAddress}&limit=1`);
const data = await response.json();
if (data && data.length > 0) {
const lat = parseFloat(data[0].lat);
const lon = parseFloat(data[0].lon);
return { lat, lon };
} else {
console.warn('Could not geocode address:', address);
// Return null if geocoding fails (don't include Job_Address)
return null;
}
} catch (error) {
console.error('Geocoding error:', error);
// Return null if geocoding fails (don't include Job_Address)
return null;
}
}
async function getNextJobNumber() {
try {
// Use getFirstListItem to get only the highest job number (most efficient)
// Sort by Job_Number descending to get the highest first
const highestJob = await pb.collection('Jobs').getFirstListItem('', {
sort: '-Job_Number'
});
const highestJobNumber = highestJob.Job_Number || 0;
const nextJobNumber = highestJobNumber + 1;
// Ensure it's within 1000-9999 range
if (nextJobNumber < 1000) {
return 1000;
} else if (nextJobNumber > 9999) {
throw new Error('Maximum job number (9999) reached');
}
return nextJobNumber;
} catch (error) {
// If no records exist or any other error, start with 1000
if (error.status === 404 || error.message?.includes('not found')) {
// No jobs exist, start with 1000
return 1000;
}
console.error('Error getting next job number:', error);
// Fallback: start with 1000 if we can't determine the next number
return 1000;
}
}
async function debugCollections() {
try {
console.log('=== DEBUGGING COLLECTIONS ===');
// Try different collection names (only valid ones)
const collectionNames = ['jobs', 'Jobs'];
for (const collectionName of collectionNames) {
try {
console.log(`Trying collection: ${collectionName}`);
const records = await pb.collection(collectionName).getList(1, 10);
console.log(`${collectionName} found with ${records.totalItems} items`);
if (records.totalItems > 0) {
console.log(`${collectionName} sample item:`, records.items[0]);
}
} catch (error) {
console.log(`${collectionName} error:`, error.message);
}
}
console.log('=== END DEBUG ===');
} catch (error) {
console.warn('Debug collections failed:', error.message);
}
}
async function loadCollectionSchema() {
// Try to fetch real schema from PocketBase
try {
// Use API key for schema access if available (bypasses collection rules)
let collection;
if (API_KEY) {
// Make request with API key in header
const response = await fetch(`${pb.baseUrl}/api/collections/Jobs`, {
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
collection = await response.json();
} else {
// Use standard PocketBase client (may fail with 403 if no permissions)
collection = await pb.collections.getOne('Jobs');
}
console.log('Fetched collection:', collection);
console.log('Collection schema:', collection.schema);
if (collection.schema && collection.schema.length > 0) {
// Map PocketBase schema to our format
collectionSchema = collection.schema.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
options: field.options || {}
}));
console.log('Real collection schema loaded with', collectionSchema.length, 'fields');
console.log('Schema details:', collectionSchema.map(f => ({
name: f.name,
type: f.type,
required: f.required
})));
} else {
throw new Error('Collection schema is empty');
}
} catch (error) {
console.warn('Could not fetch collection schema, using defaults:', error.message);
// Fallback to predefined schema
collectionSchema = [
{ name: 'Job_Number', type: 'number' },
{ name: 'Job_Name', type: 'text' },
{ name: 'Job_Address', type: 'text' },
{ name: 'updated', type: 'date' }
];
console.log('Using predefined collection schema with', collectionSchema.length, 'fields');
}
updateUIBasedOnSchema();
}
function updateUIBasedOnSchema() {
// Schema is now fixed with just Job_Number, Job_Full_Name, and updated
// No dynamic UI updates needed
}
async function loadJobs() {
try {
showLoading(true);
console.log('Attempting to load jobs from collection...');
// Fetch jobs from PocketBase
const records = await pb.collection('jobs').getList(1, 200, {
const collectionUsed = 'Jobs';
// Use getFullList to fetch all records at once (better for client-side filtering)
// This provides instant filtering without server round-trips
const records = await pb.collection(collectionUsed).getFullList({
sort: '-updated'
});
allJobs = records.items;
allJobs = records || [];
filteredJobs = [...allJobs];
console.log(`Jobs response from collection "${collectionUsed}":`, records);
console.log('Number of jobs found:', allJobs.length);
// Debug: Log each job's structure
if (allJobs.length > 0) {
console.log('Sample job structure:', allJobs[0]);
console.log('Job fields:', Object.keys(allJobs[0]));
}
// Apply existing search filter if any
const searchInput = document.getElementById('search-input');
if (searchInput && searchInput.value.trim()) {
filterJobs();
} else {
displayJobs();
updatePagination();
}
if (allJobs.length === 0) {
console.log('No jobs found - showing empty state');
} else {
console.log(`Loaded ${allJobs.length} job(s) successfully from collection "${collectionUsed}"`);
}
} catch (error) {
console.error('Error loading jobs:', error.message);
showError('Failed to load jobs. Please try again.');
console.error('Error loading jobs:', error);
console.error('Error details:', {
message: error.message,
status: error.response?.status,
data: error.response?.data,
url: error.config?.url
});
// Handle specific error cases
if (error.response?.status === 403) {
showError('Access denied to jobs collection. Please check your permissions in PocketBase.');
} else if (error.response?.status === 404) {
showError('Jobs collection not found. Please create the "jobs" collection in PocketBase. Tried: jobs, Jobs');
} else {
showError(`Failed to load jobs: ${error.message}`);
}
// Show empty state
allJobs = [];
filteredJobs = [];
displayJobs();
updatePagination();
} finally {
showLoading(false);
}
@@ -507,66 +848,127 @@
function displayJobs() {
const tbody = document.getElementById('jobs-tbody');
const thead = document.getElementById('table-headers');
const emptyState = document.getElementById('empty-state');
console.log('displayJobs called with filteredJobs:', filteredJobs);
console.log('filteredJobs length:', filteredJobs.length);
if (filteredJobs.length === 0) {
console.log('No jobs to display, showing empty state');
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
console.log('Hiding empty state and displaying jobs');
emptyState.style.display = 'none';
// Generate table headers dynamically
if (collectionSchema) {
const headerHTML = collectionSchema.map(field => {
let displayName;
// Custom display names for specific fields
switch(field.name) {
case 'Job_Number':
displayName = 'Job Number';
break;
case 'Job_Name':
displayName = 'Job Name';
break;
case 'Job_Address':
displayName = 'Address';
break;
default:
// Default formatting: replace underscores with spaces and capitalize words
displayName = field.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
return `<th>${displayName}</th>`;
}).join('') + '<th>Actions</th>';
thead.innerHTML = headerHTML;
}
// Get jobs for current page
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const pageJobs = filteredJobs.slice(startIndex, endIndex);
tbody.innerHTML = pageJobs.map(job => `
<tr>
<td>
<div class="job-name">${job.name || 'Unnamed Job'}</div>
</td>
<td>${job.client || 'N/A'}</td>
<td>
<span class="job-status status-${job.status || 'active'}">
${job.status || 'active'}
</span>
</td>
<td>
<span class="job-budget">$${job.budget ? job.budget.toLocaleString() : '0'}</span>
</td>
<td>
<div class="job-dates">${job.start_date ? formatDate(job.start_date) : 'Not set'}</div>
</td>
<td>
<div class="job-dates">${job.end_date ? formatDate(job.end_date) : 'Not set'}</div>
</td>
<td>
<div class="job-dates">${formatDate(job.updated)}</div>
</td>
console.log('Displaying pageJobs:', pageJobs);
tbody.innerHTML = pageJobs.map(job => {
console.log('Processing job:', job);
const rowHTML = collectionSchema.map(field => {
const value = job[field.name];
const formattedValue = formatFieldValue(value, field);
console.log(`Field ${field.name}: ${value} -> ${formattedValue}`);
return `<td>${formattedValue}</td>`;
}).join('') + `
<td>
<div class="job-actions">
<button class="action-btn btn-edit" onclick="editJob('${job.id}')">Edit</button>
<button class="action-btn btn-delete" onclick="deleteJob('${job.id}')">Delete</button>
</div>
</td>
</tr>
`).join('');
`;
return `<tr>${rowHTML}</tr>`;
}).join('');
}
/**
* Builds a PocketBase filter string for server-side filtering
* Useful for large datasets where server-side filtering is more efficient
*
* Example usage:
* const filter = buildPocketBaseFilter('test');
* const records = await pb.collection('Jobs').getList(1, 50, { filter });
*
* @param {string} searchTerm - The search term to filter by
* @returns {string|null} PocketBase filter string or null if no search term
*/
function buildPocketBaseFilter(searchTerm) {
if (!searchTerm || searchTerm.trim() === '') {
return null;
}
const trimmed = searchTerm.trim();
// Try to parse as number for Job_Number search
const numberMatch = parseInt(trimmed);
if (!isNaN(numberMatch)) {
// Search by Job_Number
return `Job_Number = ${numberMatch}`;
}
// Search in Job_Name (case-insensitive)
// PocketBase filter syntax: field ~ "value" for contains (case-insensitive)
return `Job_Name ~ "${trimmed}"`;
}
function filterJobs() {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const statusFilter = document.getElementById('status-filter').value;
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
// Client-side filtering for instant results
filteredJobs = allJobs.filter(job => {
const matchesSearch = !searchTerm ||
(job.name && job.name.toLowerCase().includes(searchTerm)) ||
(job.client && job.client.toLowerCase().includes(searchTerm));
if (!searchTerm) {
return true;
}
const matchesStatus = !statusFilter || job.status === statusFilter;
// Search across Job_Number, Job_Name, and Job_Address
const matchesSearch =
(job.Job_Number && job.Job_Number.toString().toLowerCase().includes(searchTerm)) ||
(job.Job_Name && job.Job_Name.toLowerCase().includes(searchTerm)) ||
(job.Job_Address && (
// Handle Job_Address as object {lat, lon} or string
(typeof job.Job_Address === 'object' && job.Job_Address !== null
? `${job.Job_Address.lat},${job.Job_Address.lon}`.toLowerCase()
: String(job.Job_Address).toLowerCase()
).includes(searchTerm)
));
return matchesSearch && matchesStatus;
return matchesSearch;
});
currentPage = 1;
@@ -604,6 +1006,33 @@
}
}
function formatFieldValue(value, field) {
if (value === null || value === undefined || value === '') {
return 'N/A';
}
switch (field.type) {
case 'date':
return formatDate(value);
case 'number':
// Job_Number should be displayed as plain number
return Number(value).toString();
default:
// Handle text fields - Job_Name gets special styling
if (field.name === 'Job_Name') {
return `<div class="job-name">${value}</div>`;
}
// Handle Job_Address as object {lat, lon}
if (field.name === 'Job_Address' && typeof value === 'object' && value !== null) {
if (value.lat !== undefined && value.lon !== undefined) {
return `${value.lat.toFixed(6)}, ${value.lon.toFixed(6)}`;
}
return JSON.stringify(value);
}
return value;
}
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
@@ -617,9 +1046,254 @@
await loadJobs();
}
function showAddJobModal() {
// TODO: Implement add job modal
alert('Add job functionality coming soon!');
async function showAddJobModal() {
// Generate next job number
const nextJobNumber = await getNextJobNumber();
// Create overlay popup HTML
const modalHTML = `
<div id="job-modal" class="modal-overlay">
<div class="modal-content popup-content">
<div class="modal-header">
<h2>Add New Job</h2>
<button class="modal-close" onclick="closeJobModal()">×</button>
</div>
<form id="job-form" onsubmit="submitJobForm(event)">
<div class="form-group">
<label for="job-number">Job Number</label>
<input type="number" id="job-number" value="${nextJobNumber}" readonly required>
</div>
<div class="form-group">
<label for="job-name">Job Name</label>
<input type="text" id="job-name" placeholder="Enter job name" required>
</div>
<div class="form-group">
<label for="job-address">Address or Coordinates</label>
<input type="text" id="job-address" placeholder="Enter address (e.g., '123 Main St') or coordinates (e.g., '40.7128,-74.0060')">
<small style="color: var(--gruvbox-fg2); font-size: 0.8rem; display: block; margin-top: 0.25rem;">
Addresses will be converted to coordinates automatically
</small>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeJobModal()">Cancel</button>
<button type="submit" class="btn btn-primary" id="submit-btn">
<span id="submit-text">Create Job</span>
<span id="submit-loading" class="loading" style="display: none;"></span>
</button>
</div>
</form>
</div>
</div>
`;
// Add modal to page
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Focus on first input
setTimeout(() => {
document.getElementById('job-name').focus();
}, 100);
}
function closeJobModal() {
const modal = document.getElementById('job-modal');
if (modal) {
modal.remove();
}
}
async function submitJobForm(event) {
event.preventDefault();
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
const submitLoading = document.getElementById('submit-loading');
// Show loading state
submitBtn.disabled = true;
submitText.style.display = 'none';
submitLoading.style.display = 'inline-block';
try {
// Collect form data
const jobName = document.getElementById('job-name').value.trim();
const jobAddress = document.getElementById('job-address').value.trim();
// Convert address to coordinates if provided
let jobAddressCoords = null;
if (jobAddress) {
jobAddressCoords = await addressToCoordinates(jobAddress);
console.log('Address converted to coordinates:', jobAddress, '->', jobAddressCoords);
}
const jobData = {
Job_Number: parseInt(document.getElementById('job-number').value),
Job_Name: jobName
};
// Only add Job_Address if it was provided and successfully converted
// Job_Address must be an object with {lat, lon} format
if (jobAddressCoords && typeof jobAddressCoords === 'object') {
jobData.Job_Address = jobAddressCoords;
}
console.log('Sending job data:', jobData);
console.log('Job_Number type:', typeof jobData.Job_Number, 'value:', jobData.Job_Number);
console.log('Job_Name type:', typeof jobData.Job_Name, 'value:', jobData.Job_Name);
if (jobData.Job_Address) {
console.log('Job_Address:', jobData.Job_Address);
}
// Check if job number already exists (optional validation)
try {
const existingJobs = await pb.collection('Jobs').getList(1, 1, {
filter: `Job_Number = ${jobData.Job_Number}`
});
if (existingJobs.items.length > 0) {
throw new Error(`Job number ${jobData.Job_Number} already exists. Please refresh to get the next available number.`);
}
} catch (checkError) {
if (checkError.message.includes('already exists')) {
throw checkError;
}
// Ignore other errors from the check - continue with creation
console.warn('Could not check for existing job number:', checkError.message);
}
// Log the exact data structure being sent
console.log('=== CREATING JOB ===');
console.log('Collection: Jobs');
console.log('Data being sent:', JSON.stringify(jobData, null, 2));
console.log('Data types:', Object.entries(jobData).map(([k, v]) => `${k}: ${typeof v} = ${v}`));
// Try to get collection info for comparison
try {
let collection;
if (API_KEY) {
// Use API key for schema access
const response = await fetch(`${pb.baseUrl}/api/collections/Jobs`, {
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});
if (response.ok) {
collection = await response.json();
} else {
throw new Error(`HTTP ${response.status}`);
}
} else {
collection = await pb.collections.getOne('Jobs');
}
console.log('Collection fields:', collection.schema?.map(f => ({
name: f.name,
type: f.type,
required: f.required
})));
} catch (e) {
console.warn('Could not fetch collection info:', e.message);
}
// Create job in PocketBase
const createdJob = await pb.collection('Jobs').create(jobData);
console.log('Job created successfully:', createdJob);
// Close modal and refresh jobs list
closeJobModal();
await loadJobs();
// Show success message (you could implement a toast notification here)
alert('Job created successfully!');
} catch (error) {
console.error('Error creating job:', error);
console.error('Full error object:', JSON.stringify(error, null, 2));
// Show detailed error information
let errorMessage = 'Failed to create job.';
// PocketBase errors have different structures - check all possible locations
if (error.response) {
console.error('Error response:', error.response);
console.error('Error response.data:', error.response.data);
console.error('Error response.status:', error.response.status);
console.error('Error response.url:', error.response.url);
const errorData = error.response.data || {};
// Check for validation errors in different possible locations
if (errorData.data) {
const validationErrors = errorData.data;
console.error('Validation errors found:', validationErrors);
const errorDetails = Object.entries(validationErrors)
.map(([field, messages]) => {
const msg = Array.isArray(messages) ? messages.join(', ') : String(messages);
return `${field}: ${msg}`;
})
.join('\n');
errorMessage += '\n\nValidation errors:\n' + errorDetails;
}
// Check if errorData itself is an object with field-level errors
if (typeof errorData === 'object' && !errorData.message && !errorData.data) {
const fieldErrors = Object.entries(errorData)
.filter(([key]) => key !== 'code' && key !== 'status')
.map(([field, messages]) => {
const msg = Array.isArray(messages) ? messages.join(', ') : String(messages);
return `${field}: ${msg}`;
});
if (fieldErrors.length > 0) {
errorMessage += '\n\nField errors:\n' + fieldErrors.join('\n');
}
}
if (errorData.message) {
errorMessage += '\n\n' + errorData.message;
}
} else if (error.message) {
errorMessage += '\n\n' + error.message;
}
// Try to fetch collection schema for debugging
try {
let collection;
if (API_KEY) {
// Use API key for schema access
const response = await fetch(`${pb.baseUrl}/api/collections/Jobs`, {
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});
if (response.ok) {
collection = await response.json();
} else {
throw new Error(`HTTP ${response.status}`);
}
} else {
collection = await pb.collections.getOne('Jobs');
}
console.log('Collection schema:', collection);
console.log('Collection fields:', collection.schema?.map(f => ({
name: f.name,
type: f.type,
required: f.required,
options: f.options
})));
} catch (schemaError) {
console.warn('Could not fetch collection schema:', schemaError);
}
alert(errorMessage);
// Also suggest running the debug function
console.log('💡 Try running debugPocketBase() in the console to test permissions');
} finally {
// Reset loading state
submitBtn.disabled = false;
submitText.style.display = 'inline';
submitLoading.style.display = 'none';
}
}
function editJob(jobId) {
@@ -633,7 +1307,7 @@
}
try {
await pb.collection('jobs').delete(jobId);
await pb.collection('Jobs').delete(jobId);
await loadJobs(); // Refresh the list
} catch (error) {
console.error('Error deleting job:', error.message);
@@ -661,13 +1335,64 @@
window.logout = logout;
window.refreshJobs = refreshJobs;
window.showAddJobModal = showAddJobModal;
window.closeJobModal = closeJobModal;
window.submitJobForm = submitJobForm;
window.editJob = editJob;
window.deleteJob = deleteJob;
window.changePage = changePage;
window.filterJobs = filterJobs;
// Debug function available in console
window.debugPocketBase = async function() {
console.log('=== POCKETBASE DEBUG ===');
console.log('Base URL:', pb.baseUrl);
console.log('Auth valid:', pb.authStore.isValid);
console.log('Auth model:', pb.authStore.model);
try {
// Try to get collections
const collections = await pb.collections.getFullList();
console.log('Available collections:', collections.map(c => c.name));
} catch (error) {
console.error('Cannot list collections:', error.message);
}
// Test the Jobs collection specifically
console.log('Testing Jobs collection...');
// Test read access
try {
const result = await pb.collection('Jobs').getList(1, 10);
console.log(`✅ Read access: ${result.totalItems} items found`);
if (result.items.length > 0) {
console.log('Sample item:', result.items[0]);
}
} catch (error) {
console.log('❌ Read access failed:', error.message);
}
// Test write access with dummy data
try {
const testData = { Job_Number: 9999, Job_Name: 'TEST_JOB_PLEASE_DELETE' };
const testResult = await pb.collection('Jobs').create(testData);
console.log('✅ Write access successful, created test record:', testResult);
// Clean up test record
await pb.collection('Jobs').delete(testResult.id);
console.log('✅ Test record cleaned up');
} catch (error) {
console.log('❌ Write access failed:', error.message);
if (error.response) {
console.log('Error details:', error.response.data);
}
}
console.log('=== END DEBUG ===');
};
// Initialize on page load
document.addEventListener('DOMContentLoaded', initJobsPage);
</script>
</body>
</html>