- Updated project creation modal to allow selection of optional modules (Sound and Vibration Monitoring). - Modified project dashboard and header to display active modules and provide options to add/remove them. - Enhanced project detail view to dynamically adjust UI based on enabled modules. - Implemented a new migration script to create a `project_modules` table and seed it based on existing project types. - Adjusted form submissions to handle module selections and ensure proper API interactions for module management.
299 lines
16 KiB
HTML
299 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Projects - Terra-View{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-8 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Projects</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Manage monitoring projects, locations, and schedules</p>
|
|
</div>
|
|
<button onclick="showCreateProjectModal()"
|
|
class="px-6 py-3 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg font-medium transition-colors">
|
|
<svg class="w-5 h-5 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
</svg>
|
|
New Project
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8"
|
|
hx-get="/api/projects/stats"
|
|
hx-trigger="load, every 30s"
|
|
hx-swap="innerHTML">
|
|
<!-- Stats will be loaded here -->
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg mb-6">
|
|
<div class="border-b border-gray-200 dark:border-gray-700">
|
|
<nav class="flex space-x-8 px-6" aria-label="Tabs">
|
|
<button onclick="switchTab('all')"
|
|
id="tab-all"
|
|
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
|
|
All Projects
|
|
</button>
|
|
<button onclick="switchTab('upcoming')"
|
|
id="tab-upcoming"
|
|
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
|
|
Upcoming
|
|
</button>
|
|
<button onclick="switchTab('active')"
|
|
id="tab-active"
|
|
class="tab-button border-b-2 border-seismo-orange text-seismo-orange px-1 py-4 text-sm font-medium">
|
|
Active
|
|
</button>
|
|
<button onclick="switchTab('on_hold')"
|
|
id="tab-on_hold"
|
|
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
|
|
On Hold
|
|
</button>
|
|
<button onclick="switchTab('completed')"
|
|
id="tab-completed"
|
|
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
|
|
Completed
|
|
</button>
|
|
<button onclick="switchTab('archived')"
|
|
id="tab-archived"
|
|
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
|
|
Archived
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Projects List -->
|
|
<div id="projects-list"
|
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
hx-get="/api/projects/list?status=active"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
<!-- Loading skeletons -->
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-64 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-64 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-64 rounded-xl"></div>
|
|
</div>
|
|
|
|
<!-- Create Project Modal -->
|
|
<div id="createProjectModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-start justify-between">
|
|
<div>
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Create New Project</h2>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Select a project type and configure settings</p>
|
|
</div>
|
|
<button onclick="hideCreateProjectModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 ml-4">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6" id="createProjectContent">
|
|
<form id="createProjectFormElement">
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Project Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text"
|
|
name="name"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Project Number
|
|
<span class="text-gray-400 font-normal">(xxxx-YY)</span>
|
|
</label>
|
|
<input type="text"
|
|
name="project_number"
|
|
pattern="\d{4}-\d{2}"
|
|
placeholder="2567-23"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Client Name
|
|
</label>
|
|
<input type="text"
|
|
name="client_name"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea name="description"
|
|
rows="2"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"></textarea>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Site Address
|
|
</label>
|
|
<input type="text"
|
|
name="site_address"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Site Coordinates
|
|
<span class="text-gray-400 font-normal">(optional)</span>
|
|
</label>
|
|
<input type="text"
|
|
name="site_coordinates"
|
|
placeholder="40.7128,-74.0060"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start Date</label>
|
|
<input type="date" name="start_date"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">End Date <span class="text-gray-400 font-normal">(optional)</span></label>
|
|
<input type="date" name="end_date"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modules -->
|
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Add Modules
|
|
<span class="text-gray-400 font-normal">(optional — can be added later)</span>
|
|
</label>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<label class="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-orange-400 has-[:checked]:border-orange-400 has-[:checked]:bg-orange-50 dark:has-[:checked]:bg-orange-900/20 transition-colors">
|
|
<input type="checkbox" id="ov-module-sound" class="accent-seismo-orange">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white">Sound Monitoring</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">SLMs, sessions, reports</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-blue-400 has-[:checked]:border-blue-400 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20 transition-colors">
|
|
<input type="checkbox" id="ov-module-vibration" class="accent-blue-500">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white">Vibration Monitoring</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Seismographs, modems</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ov-create-error" class="hidden mt-3 p-3 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg text-sm"></div>
|
|
|
|
<div class="mt-6 flex justify-end space-x-3">
|
|
<button type="button"
|
|
onclick="hideCreateProjectModal()"
|
|
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" id="ov-submit-btn"
|
|
class="px-6 py-2 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg font-medium transition-colors">
|
|
Create Project
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Tab switching
|
|
function switchTab(status) {
|
|
// Update tab styling
|
|
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
btn.classList.remove('border-seismo-orange', 'text-seismo-orange');
|
|
btn.classList.add('border-transparent', 'text-gray-500');
|
|
});
|
|
|
|
const activeTab = document.getElementById(`tab-${status}`);
|
|
activeTab.classList.remove('border-transparent', 'text-gray-500');
|
|
activeTab.classList.add('border-seismo-orange', 'text-seismo-orange');
|
|
|
|
// Load projects for this status
|
|
const statusParam = status === 'all' ? '' : `?status=${status}`;
|
|
htmx.ajax('GET', `/api/projects/list${statusParam}`, {target: '#projects-list'});
|
|
}
|
|
|
|
// Modal controls
|
|
function showCreateProjectModal() {
|
|
document.getElementById('createProjectModal').classList.remove('hidden');
|
|
}
|
|
|
|
function showCreateProjectModal() {
|
|
document.getElementById('createProjectModal').classList.remove('hidden');
|
|
document.getElementById('createProjectFormElement').reset();
|
|
document.getElementById('ov-create-error').classList.add('hidden');
|
|
}
|
|
|
|
function hideCreateProjectModal() {
|
|
document.getElementById('createProjectModal').classList.add('hidden');
|
|
}
|
|
|
|
document.getElementById('createProjectFormElement').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
const submitBtn = document.getElementById('ov-submit-btn');
|
|
const errorDiv = document.getElementById('ov-create-error');
|
|
errorDiv.classList.add('hidden');
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Creating...';
|
|
|
|
const formData = new FormData(this);
|
|
// project_type_id no longer required — send empty string so backend accepts it
|
|
formData.set('project_type_id', '');
|
|
|
|
try {
|
|
const resp = await fetch('/api/projects/create', { method: 'POST', body: formData });
|
|
const result = await resp.json();
|
|
if (!resp.ok || !result.success) {
|
|
errorDiv.textContent = result.detail || result.message || 'Failed to create project';
|
|
errorDiv.classList.remove('hidden');
|
|
return;
|
|
}
|
|
const projectId = result.project_id;
|
|
// Add selected modules
|
|
if (document.getElementById('ov-module-sound').checked) {
|
|
await fetch(`/api/projects/${projectId}/modules`, {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({ module_type: 'sound_monitoring' }),
|
|
});
|
|
}
|
|
if (document.getElementById('ov-module-vibration').checked) {
|
|
await fetch(`/api/projects/${projectId}/modules`, {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({ module_type: 'vibration_monitoring' }),
|
|
});
|
|
}
|
|
hideCreateProjectModal();
|
|
htmx.ajax('GET', '/api/projects/list', {target: '#projects-list'});
|
|
} catch(err) {
|
|
errorDiv.textContent = `Error: ${err.message}`;
|
|
errorDiv.classList.remove('hidden');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Create Project';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|