feat: Refactor project creation and management to support modular project types
- 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.
This commit is contained in:
@@ -770,7 +770,7 @@
|
||||
<script>
|
||||
const projectId = "{{ project_id }}";
|
||||
let editingLocationId = null;
|
||||
let projectTypeId = null;
|
||||
let projectModules = []; // list of enabled module_type strings, e.g. ['sound_monitoring']
|
||||
|
||||
async function quickUpdateStatus(newStatus) {
|
||||
try {
|
||||
@@ -828,7 +828,7 @@ async function loadProjectDetails() {
|
||||
throw new Error('Failed to load project details');
|
||||
}
|
||||
const data = await response.json();
|
||||
projectTypeId = data.project_type_id || null;
|
||||
projectModules = data.modules || [];
|
||||
|
||||
// Update breadcrumb
|
||||
document.getElementById('project-name-breadcrumb').textContent = data.name || 'Project';
|
||||
@@ -849,10 +849,10 @@ async function loadProjectDetails() {
|
||||
if (modeRadio) modeRadio.checked = true;
|
||||
settingsUpdateModeStyles();
|
||||
|
||||
// Update tab labels and visibility based on project type
|
||||
const isSoundProject = projectTypeId === 'sound_monitoring';
|
||||
const isVibrationProject = projectTypeId === 'vibration_monitoring';
|
||||
if (isSoundProject) {
|
||||
// Update tab labels and visibility based on active modules
|
||||
const hasSoundModule = projectModules.includes('sound_monitoring');
|
||||
const hasVibrationModule = projectModules.includes('vibration_monitoring');
|
||||
if (hasSoundModule && !hasVibrationModule) {
|
||||
document.getElementById('locations-tab-label').textContent = 'NRLs';
|
||||
document.getElementById('locations-header').textContent = 'Noise Recording Locations';
|
||||
document.getElementById('add-location-label').textContent = 'Add NRL';
|
||||
@@ -860,11 +860,11 @@ async function loadProjectDetails() {
|
||||
// Monitoring Sessions and Data Files tabs are sound-only
|
||||
// Data Files also hides the FTP browser section for manual projects
|
||||
const isRemote = mode === 'remote';
|
||||
document.getElementById('sessions-tab-btn').classList.toggle('hidden', !isSoundProject);
|
||||
document.getElementById('data-tab-btn').classList.toggle('hidden', !isSoundProject);
|
||||
// Schedules and Assigned Units: hidden for vibration; for sound, only show if remote
|
||||
document.getElementById('schedules-tab-btn')?.classList.toggle('hidden', isVibrationProject || (isSoundProject && !isRemote));
|
||||
document.getElementById('units-tab-btn')?.classList.toggle('hidden', isVibrationProject || (isSoundProject && !isRemote));
|
||||
document.getElementById('sessions-tab-btn').classList.toggle('hidden', !hasSoundModule);
|
||||
document.getElementById('data-tab-btn').classList.toggle('hidden', !hasSoundModule);
|
||||
// Schedules and Assigned Units: hidden when no sound module; for sound, only show if remote
|
||||
document.getElementById('schedules-tab-btn')?.classList.toggle('hidden', !hasSoundModule || !isRemote);
|
||||
document.getElementById('units-tab-btn')?.classList.toggle('hidden', !hasSoundModule || !isRemote);
|
||||
// FTP browser within Data Files tab
|
||||
document.getElementById('ftp-browser')?.classList.toggle('hidden', !isRemote);
|
||||
|
||||
@@ -996,11 +996,13 @@ function openLocationModal(defaultType) {
|
||||
if (connectedRadio) { connectedRadio.checked = true; updateModeLabels(); }
|
||||
const locationTypeSelect = document.getElementById('location-type');
|
||||
const locationTypeWrapper = locationTypeSelect.closest('div');
|
||||
if (projectTypeId === 'sound_monitoring') {
|
||||
const hasSoundMod = projectModules.includes('sound_monitoring');
|
||||
const hasVibMod = projectModules.includes('vibration_monitoring');
|
||||
if (hasSoundMod && !hasVibMod) {
|
||||
locationTypeSelect.value = 'sound';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
} else if (projectTypeId === 'vibration_monitoring') {
|
||||
} else if (hasVibMod && !hasSoundMod) {
|
||||
locationTypeSelect.value = 'vibration';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
@@ -1030,11 +1032,13 @@ function openEditLocationModal(button) {
|
||||
if (modeRadio) { modeRadio.checked = true; updateModeLabels(); }
|
||||
const locationTypeSelect = document.getElementById('location-type');
|
||||
const locationTypeWrapper = locationTypeSelect.closest('div');
|
||||
if (projectTypeId === 'sound_monitoring') {
|
||||
const hasSoundModE = projectModules.includes('sound_monitoring');
|
||||
const hasVibModE = projectModules.includes('vibration_monitoring');
|
||||
if (hasSoundModE && !hasVibModE) {
|
||||
locationTypeSelect.value = 'sound';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
} else if (projectTypeId === 'vibration_monitoring') {
|
||||
} else if (hasVibModE && !hasSoundModE) {
|
||||
locationTypeSelect.value = 'vibration';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
@@ -1060,9 +1064,9 @@ document.getElementById('location-form').addEventListener('submit', async functi
|
||||
const address = document.getElementById('location-address').value.trim();
|
||||
const coordinates = document.getElementById('location-coordinates').value.trim();
|
||||
let locationType = document.getElementById('location-type').value;
|
||||
if (projectTypeId === 'sound_monitoring') {
|
||||
if (projectModules.includes('sound_monitoring') && !projectModules.includes('vibration_monitoring')) {
|
||||
locationType = 'sound';
|
||||
} else if (projectTypeId === 'vibration_monitoring') {
|
||||
} else if (projectModules.includes('vibration_monitoring') && !projectModules.includes('sound_monitoring')) {
|
||||
locationType = 'vibration';
|
||||
}
|
||||
|
||||
|
||||
@@ -96,124 +96,122 @@
|
||||
</div>
|
||||
|
||||
<div class="p-6" id="createProjectContent">
|
||||
<!-- Step 1: Project Type Selection (initially shown) -->
|
||||
<div id="projectTypeSelection">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Choose Project Type</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
hx-get="/api/projects/types/list"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML">
|
||||
<!-- Project type cards will be loaded here -->
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-48 rounded-lg"></div>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-48 rounded-lg"></div>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-48 rounded-lg"></div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Step 2: Project Details Form (hidden initially) -->
|
||||
<div id="projectDetailsForm" class="hidden">
|
||||
<button onclick="backToTypeSelection()"
|
||||
class="mb-4 text-seismo-orange hover:text-seismo-navy">
|
||||
← Back to project types
|
||||
</button>
|
||||
|
||||
<form id="createProjectFormElement"
|
||||
hx-post="/api/projects/create"
|
||||
hx-swap="none">
|
||||
<input type="hidden" id="project_type_id" name="project_type_id">
|
||||
|
||||
<div class="space-y-4">
|
||||
<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 Name *
|
||||
Project Number
|
||||
<span class="text-gray-400 font-normal">(xxxx-YY)</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">
|
||||
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">
|
||||
Description
|
||||
Client Name
|
||||
</label>
|
||||
<textarea name="description"
|
||||
rows="3"
|
||||
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"></textarea>
|
||||
<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 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">
|
||||
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">
|
||||
</div>
|
||||
|
||||
<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">
|
||||
</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">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
End Date (Optional)
|
||||
</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">
|
||||
</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 Coordinates (Optional)
|
||||
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">
|
||||
<p class="text-xs text-gray-500 mt-1">Format: latitude,longitude</p>
|
||||
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="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"
|
||||
class="px-6 py-2 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg font-medium">
|
||||
Create Project
|
||||
</button>
|
||||
<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>
|
||||
</form>
|
||||
</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>
|
||||
@@ -241,31 +239,58 @@ 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('projectTypeSelection').classList.remove('hidden');
|
||||
document.getElementById('projectDetailsForm').classList.add('hidden');
|
||||
}
|
||||
|
||||
function selectProjectType(typeId, typeName) {
|
||||
document.getElementById('project_type_id').value = typeId;
|
||||
document.getElementById('projectTypeSelection').classList.add('hidden');
|
||||
document.getElementById('projectDetailsForm').classList.remove('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...';
|
||||
|
||||
function backToTypeSelection() {
|
||||
document.getElementById('projectTypeSelection').classList.remove('hidden');
|
||||
document.getElementById('projectDetailsForm').classList.add('hidden');
|
||||
}
|
||||
const formData = new FormData(this);
|
||||
// project_type_id no longer required — send empty string so backend accepts it
|
||||
formData.set('project_type_id', '');
|
||||
|
||||
// Handle form submission success
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.elt.id === 'createProjectFormElement' && event.detail.successful) {
|
||||
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();
|
||||
// Refresh project list
|
||||
htmx.ajax('GET', '/api/projects/list', {target: '#projects-list'});
|
||||
// Show success message
|
||||
alert('Project created successfully!');
|
||||
} catch(err) {
|
||||
errorDiv.textContent = `Error: ${err.message}`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Create Project';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user