Files

1123 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRISM Jobs - Construction Job Management</title>
<style>
:root {
--gruvbox-bg0: #282828;
--gruvbox-bg1: #3c3836;
--gruvbox-bg2: #504945;
--gruvbox-bg3: #665c54;
--gruvbox-fg0: #fbf1c7;
--gruvbox-fg1: #ebdbb2;
--gruvbox-fg2: #d5c4a1;
--gruvbox-orange: #fe8019;
--gruvbox-green: #b8bb26;
--gruvbox-blue: #83a598;
--gruvbox-purple: #d3869b;
--gruvbox-red: #fb4934;
--gruvbox-yellow: #fabd2f;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Display', 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--gruvbox-bg0) 0%, var(--gruvbox-bg2) 50%, var(--gruvbox-bg1) 100%);
min-height: 100vh;
color: var(--gruvbox-fg1);
}
.main-content {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: var(--gruvbox-orange);
}
.actions {
display: flex;
gap: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--gruvbox-blue) 0%, var(--gruvbox-purple) 100%);
color: var(--gruvbox-bg0);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(131, 165, 152, 0.3);
}
.btn-secondary {
background: var(--gruvbox-bg2);
color: var(--gruvbox-fg1);
border: 1px solid var(--gruvbox-bg3);
}
.btn-secondary:hover {
background: var(--gruvbox-bg3);
transform: translateY(-1px);
}
.jobs-table-container {
background: var(--gruvbox-bg1);
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
border: 1px solid var(--gruvbox-bg3);
backdrop-filter: blur(10px);
overflow: hidden;
}
.table-header {
padding: 1.5rem;
border-bottom: 1px solid var(--gruvbox-bg3);
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--gruvbox-fg0);
}
.search-filter {
display: flex;
gap: 1rem;
align-items: center;
}
.search-input {
padding: 0.5rem 1rem;
border: 2px solid var(--gruvbox-bg3);
border-radius: 6px;
background: var(--gruvbox-bg2);
color: var(--gruvbox-fg1);
font-size: 0.9rem;
}
.search-input:focus {
outline: none;
border-color: var(--gruvbox-blue);
box-shadow: 0 0 0 3px rgba(131, 165, 152, 0.1);
}
.filter-select {
padding: 0.5rem;
border: 2px solid var(--gruvbox-bg3);
border-radius: 6px;
background: var(--gruvbox-bg2);
color: var(--gruvbox-fg1);
font-size: 0.9rem;
}
.jobs-table {
width: 100%;
border-collapse: collapse;
}
.jobs-table th,
.jobs-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--gruvbox-bg3);
}
.jobs-table th {
background: var(--gruvbox-bg2);
color: var(--gruvbox-fg0);
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
z-index: 10;
}
.jobs-table tbody tr {
transition: background 0.3s ease;
}
.jobs-table tbody tr:hover {
background: rgba(131, 165, 152, 0.05);
}
.job-name {
font-weight: 600;
color: var(--gruvbox-fg0);
}
.job-status {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background: rgba(184, 187, 38, 0.2);
color: var(--gruvbox-green);
}
.status-completed {
background: rgba(131, 165, 152, 0.2);
color: var(--gruvbox-blue);
}
.status-paused {
background: rgba(250, 173, 47, 0.2);
color: var(--gruvbox-yellow);
}
.status-cancelled {
background: rgba(251, 73, 52, 0.2);
color: var(--gruvbox-red);
}
.job-budget {
font-weight: 600;
color: var(--gruvbox-green);
}
.job-dates {
color: var(--gruvbox-fg2);
font-size: 0.9rem;
}
.job-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
padding: 0.375rem 0.75rem;
border: none;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-edit {
background: var(--gruvbox-blue);
color: var(--gruvbox-bg0);
}
.btn-edit:hover {
background: var(--gruvbox-purple);
transform: translateY(-1px);
}
.btn-delete {
background: var(--gruvbox-red);
color: var(--gruvbox-fg0);
}
.btn-delete:hover {
background: var(--gruvbox-orange);
transform: translateY(-1px);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 3rem;
flex-direction: column;
gap: 1rem;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid var(--gruvbox-bg3);
border-top: 4px solid var(--gruvbox-orange);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: rgba(251, 73, 52, 0.1);
color: var(--gruvbox-red);
padding: 1.25rem;
border-radius: 8px;
margin: 1rem 0;
border: 1px solid rgba(251, 73, 52, 0.3);
backdrop-filter: blur(5px);
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--gruvbox-fg2);
}
.empty-state h3 {
color: var(--gruvbox-fg1);
margin-bottom: 1rem;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
padding: 1.5rem;
border-top: 1px solid var(--gruvbox-bg3);
}
.pagination-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--gruvbox-bg3);
background: var(--gruvbox-bg2);
color: var(--gruvbox-fg1);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.pagination-btn:hover:not(:disabled) {
background: var(--gruvbox-bg3);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: var(--gruvbox-fg2);
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;
}
.page-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.actions {
justify-content: center;
}
.jobs-table {
font-size: 0.8rem;
}
.jobs-table th,
.jobs-table td {
padding: 0.5rem;
}
.search-filter {
flex-direction: column;
width: 100%;
}
.search-input,
.filter-select {
width: 100%;
}
}
</style>
</head>
<body>
<main class="main-content">
<div class="page-header">
<h1 class="page-title">Construction Jobs</h1>
<div class="actions">
<button class="btn btn-secondary" onclick="refreshJobs()">
🔄 Refresh
</button>
<button class="btn btn-primary" onclick="showAddJobModal()">
Add Job
</button>
</div>
</div>
<div class="jobs-table-container">
<div class="table-header">
<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()">
</div>
</div>
<div id="loading" class="loading">
<div class="loading-spinner"></div>
<p>Loading jobs...</p>
</div>
<div id="error-content" class="error-message" style="display: none;">
Unable to load jobs. Please try refreshing the page.
</div>
<div id="jobs-content" style="display: none;">
<table class="jobs-table" id="jobs-table">
<thead>
<tr id="table-headers">
<!-- Headers will be generated dynamically -->
</tr>
</thead>
<tbody id="jobs-tbody">
<!-- Jobs will be loaded here -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none;">
<h3>No jobs found</h3>
<p>Get started by adding your first construction job.</p>
</div>
</div>
<div class="pagination" id="pagination" style="display: none;">
<button class="pagination-btn" id="prev-btn" onclick="changePage(-1)">Previous</button>
<span class="pagination-info" id="page-info">Page 1 of 1</span>
<button class="pagination-btn" id="next-btn" onclick="changePage(1)">Next</button>
</div>
</div>
</main>
<script type="module">
import { initMenuBar, getCurrentPage } from '/components.js';
import {
isAuthenticated,
getCurrentUser,
logout,
loadApiKey,
getApiKey,
loadCollectionSchema,
getCollectionInfo,
loadJobs as dbLoadJobs,
getNextJobNumber,
createJob,
deleteJob as dbDeleteJob,
debugPocketBase,
debugCollections
} from '/db.js';
// Initialize menu bar immediately (before any async operations)
const menuBar = initMenuBar(getCurrentPage());
// State management
let allJobs = [];
let filteredJobs = [];
let currentPage = 1;
const itemsPerPage = 10;
let collectionSchema = null;
// Initialize page
async function initJobsPage() {
try {
// Check authentication
if (!isAuthenticated()) {
window.location.href = '/';
return;
}
const user = getCurrentUser();
menuBar.updateUserName(user.name || user.email);
// Debug: Check authentication and user details
console.log('Current user:', user);
console.log('Is authenticated:', isAuthenticated());
// 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
collectionSchema = await loadCollectionSchema();
await loadJobs();
} catch (error) {
console.error('Jobs page initialization failed:', error.message);
showError('Failed to initialize jobs page. Please try logging in again.');
setTimeout(() => {
window.location.href = '/';
}, 3000);
}
}
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;
}
}
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...');
const records = await dbLoadJobs();
allJobs = records || [];
filteredJobs = [...allJobs];
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`);
}
} catch (error) {
console.error('Error loading jobs:', error);
showError(error.message || 'Failed to load jobs');
// Show empty state
allJobs = [];
filteredJobs = [];
displayJobs();
updatePagination();
} finally {
showLoading(false);
}
}
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);
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>
`;
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().trim();
// Client-side filtering for instant results
filteredJobs = allJobs.filter(job => {
if (!searchTerm) {
return true;
}
// 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;
});
currentPage = 1;
displayJobs();
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredJobs.length / itemsPerPage);
const pagination = document.getElementById('pagination');
const pageInfo = document.getElementById('page-info');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (totalPages <= 1) {
pagination.style.display = 'none';
return;
}
pagination.style.display = 'flex';
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages;
}
function changePage(direction) {
const totalPages = Math.ceil(filteredJobs.length / itemsPerPage);
const newPage = currentPage + direction;
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
displayJobs();
updatePagination();
}
}
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', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
async function refreshJobs() {
await loadJobs();
}
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);
}
// 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 {
const collection = await getCollectionInfo('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 createJob(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 {
const collection = await getCollectionInfo('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) {
// TODO: Implement edit job functionality
alert(`Edit job ${jobId} - coming soon!`);
}
async function deleteJob(jobId) {
if (!confirm('Are you sure you want to delete this job?')) {
return;
}
try {
await dbDeleteJob(jobId);
await loadJobs(); // Refresh the list
} catch (error) {
console.error('Error deleting job:', error.message);
alert('Failed to delete job. Please try again.');
}
}
function handleLogout() {
logout();
window.location.href = '/';
}
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'flex' : 'none';
document.getElementById('jobs-content').style.display = show ? 'none' : 'block';
}
function showError(message) {
document.getElementById('error-content').textContent = message;
document.getElementById('error-content').style.display = 'block';
showLoading(false);
}
// Make functions globally available
window.logout = handleLogout;
window.refreshJobs = refreshJobs;
window.showAddJobModal = showAddJobModal;
window.closeJobModal = closeJobModal;
window.submitJobForm = submitJobForm;
window.editJob = editJob;
window.deleteJob = deleteJob;
window.changePage = changePage;
window.filterJobs = filterJobs;
window.debugPocketBase = debugPocketBase;
// Initialize on page load
document.addEventListener('DOMContentLoaded', initJobsPage);
</script>
</body>
</html>