Files
PRISM/public/jobs.html
T
2025-11-26 11:22:55 -06:00

674 lines
20 KiB
HTML
Raw 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;
}
@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()">
<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>
<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>
<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>
</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 PocketBase from '/pocketbase.es.mjs';
import { initMenuBar, getCurrentPage } from '/components.js';
// Initialize PocketBase
const pb = new PocketBase('https://pocketbase.ccllc.pro');
// State management
let allJobs = [];
let filteredJobs = [];
let currentPage = 1;
const itemsPerPage = 10;
// Initialize page
async function initJobsPage() {
try {
// Check authentication
if (!pb.authStore.isValid) {
window.location.href = '/';
return;
}
// Initialize menu bar
const menuBar = initMenuBar(getCurrentPage());
const user = pb.authStore.model;
menuBar.updateUserName(user.name || user.email);
// Load jobs
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 loadJobs() {
try {
showLoading(true);
// Fetch jobs from PocketBase
const records = await pb.collection('jobs').getList(1, 200, {
sort: '-updated'
});
allJobs = records.items;
filteredJobs = [...allJobs];
displayJobs();
updatePagination();
} catch (error) {
console.error('Error loading jobs:', error.message);
showError('Failed to load jobs. Please try again.');
} finally {
showLoading(false);
}
}
function displayJobs() {
const tbody = document.getElementById('jobs-tbody');
const emptyState = document.getElementById('empty-state');
if (filteredJobs.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
// 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>
<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('');
}
function filterJobs() {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const statusFilter = document.getElementById('status-filter').value;
filteredJobs = allJobs.filter(job => {
const matchesSearch = !searchTerm ||
(job.name && job.name.toLowerCase().includes(searchTerm)) ||
(job.client && job.client.toLowerCase().includes(searchTerm));
const matchesStatus = !statusFilter || job.status === statusFilter;
return matchesSearch && matchesStatus;
});
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 formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
async function refreshJobs() {
await loadJobs();
}
function showAddJobModal() {
// TODO: Implement add job modal
alert('Add job functionality coming soon!');
}
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 pb.collection('jobs').delete(jobId);
await loadJobs(); // Refresh the list
} catch (error) {
console.error('Error deleting job:', error.message);
alert('Failed to delete job. Please try again.');
}
}
function logout() {
pb.authStore.clear();
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 = logout;
window.refreshJobs = refreshJobs;
window.showAddJobModal = showAddJobModal;
window.editJob = editJob;
window.deleteJob = deleteJob;
window.changePage = changePage;
window.filterJobs = filterJobs;
// Initialize on page load
document.addEventListener('DOMContentLoaded', initJobsPage);
</script>
</body>
</html>