updated but still issues with Jobs Page api loading
This commit is contained in:
+784
-59
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user