Files
terra-view/templates/partials/project_create_modal.html
serversdown 73a6ff4d20 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.
2026-03-30 21:44:15 +00:00

302 lines
15 KiB
HTML

{#
Quick Create Project Modal
Allows inline creation of a new project from the project picker.
Include this modal in pages that use the project picker.
#}
<div id="quickCreateProjectModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-md w-full mx-4">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Create New Project</h2>
<button type="button" onclick="closeCreateProjectModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<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>
<form id="quickCreateProjectForm" class="p-6 space-y-4">
<!-- Hidden field to track which picker opened this modal -->
<input type="hidden" id="qcp-picker-id" value="">
<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"
id="qcp-project-number"
pattern="\d{4}-\d{2}"
placeholder="2567-23"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">TMI internal project number (optional)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Name <span class="text-red-500">*</span>
</label>
<input type="text"
name="client_name"
id="qcp-client-name"
required
placeholder="PJ Dick, Turner Construction, etc."
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-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">
Project Name <span class="text-red-500">*</span>
</label>
<input type="text"
name="name"
id="qcp-project-name"
required
placeholder="RKM Hall, CMU Campus, Building 7, etc."
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Site or building name</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Modules
<span class="text-gray-400 font-normal">(optional)</span>
</label>
<div class="grid grid-cols-2 gap-2">
<label class="flex items-center gap-2 p-2.5 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" name="module_sound" value="1" class="accent-seismo-orange">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white leading-tight">Sound</p>
<p class="text-xs text-gray-500 dark:text-gray-400">SLMs, sessions, reports</p>
</div>
</label>
<label class="flex items-center gap-2 p-2.5 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" name="module_vibration" value="1" class="accent-blue-500">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white leading-tight">Vibration</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Seismographs, modems</p>
</div>
</label>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Data Collection <span class="text-red-500">*</span>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-start gap-3 p-3 border-2 border-seismo-orange bg-orange-50 dark:bg-orange-900/20 rounded-lg cursor-pointer" id="qcp-mode-manual-label">
<input type="radio" name="data_collection_mode" value="manual" checked
onchange="qcpUpdateModeStyles()"
class="mt-0.5 accent-seismo-orange shrink-0">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Manual</p>
<p class="text-xs text-gray-500 dark:text-gray-400">SD card retrieved daily</p>
</div>
</label>
<label class="flex items-start gap-3 p-3 border-2 border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer" id="qcp-mode-remote-label">
<input type="radio" name="data_collection_mode" value="remote"
onchange="qcpUpdateModeStyles()"
class="mt-0.5 accent-seismo-orange shrink-0">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Remote</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Modem, data pulled via FTP</p>
</div>
</label>
</div>
</div>
<div id="qcp-error" class="hidden p-3 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg text-sm">
</div>
<div class="flex gap-3 pt-2">
<button type="submit"
id="qcp-submit-btn"
class="flex-1 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" 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>
Create & Select
</button>
<button type="button"
onclick="closeCreateProjectModal()"
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg font-medium transition-colors">
Cancel
</button>
</div>
</form>
</div>
</div>
<script>
function qcpUpdateModeStyles() {
const manualChecked = document.querySelector('input[name="data_collection_mode"][value="manual"]')?.checked;
const manualLabel = document.getElementById('qcp-mode-manual-label');
const remoteLabel = document.getElementById('qcp-mode-remote-label');
if (!manualLabel || !remoteLabel) return;
if (manualChecked) {
manualLabel.classList.add('border-seismo-orange', 'bg-orange-50', 'dark:bg-orange-900/20');
manualLabel.classList.remove('border-gray-300', 'dark:border-gray-600');
remoteLabel.classList.remove('border-seismo-orange', 'bg-orange-50', 'dark:bg-orange-900/20');
remoteLabel.classList.add('border-gray-300', 'dark:border-gray-600');
} else {
remoteLabel.classList.add('border-seismo-orange', 'bg-orange-50', 'dark:bg-orange-900/20');
remoteLabel.classList.remove('border-gray-300', 'dark:border-gray-600');
manualLabel.classList.remove('border-seismo-orange', 'bg-orange-50', 'dark:bg-orange-900/20');
manualLabel.classList.add('border-gray-300', 'dark:border-gray-600');
}
}
// Quick create project modal functions
if (typeof openCreateProjectModal === 'undefined') {
function openCreateProjectModal(searchQuery, pickerId = '') {
const modal = document.getElementById('quickCreateProjectModal');
const pickerIdInput = document.getElementById('qcp-picker-id');
const projectNumInput = document.getElementById('qcp-project-number');
const clientNameInput = document.getElementById('qcp-client-name');
const projectNameInput = document.getElementById('qcp-project-name');
const errorDiv = document.getElementById('qcp-error');
// Store which picker opened this
if (pickerIdInput) pickerIdInput.value = pickerId;
// Reset form
document.getElementById('quickCreateProjectForm').reset();
qcpUpdateModeStyles();
if (errorDiv) errorDiv.classList.add('hidden');
// Try to parse the search query intelligently
if (searchQuery) {
// Check if it looks like a project number (xxxx-YY pattern)
const projectNumMatch = searchQuery.match(/(\d{4}-\d{2})/);
if (projectNumMatch) {
if (projectNumInput) projectNumInput.value = projectNumMatch[1];
// If there's more after the number, use it as client name
const remainder = searchQuery.replace(projectNumMatch[1], '').replace(/^[\s\-]+/, '').trim();
if (remainder && clientNameInput) clientNameInput.value = remainder;
} else {
// Not a project number - assume it's client or project name
// If short (likely a name fragment), put it in client name
if (clientNameInput) clientNameInput.value = searchQuery;
}
}
// Show modal
if (modal) modal.classList.remove('hidden');
// Focus the first empty required field
if (clientNameInput && !clientNameInput.value) {
clientNameInput.focus();
} else if (projectNameInput) {
projectNameInput.focus();
}
}
function closeCreateProjectModal() {
const modal = document.getElementById('quickCreateProjectModal');
if (modal) modal.classList.add('hidden');
}
// Handle quick create form submission
document.getElementById('quickCreateProjectForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = document.getElementById('qcp-submit-btn');
const errorDiv = document.getElementById('qcp-error');
const pickerId = document.getElementById('qcp-picker-id')?.value || '';
// Show loading state
const originalBtnText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = `
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Creating...
`;
if (errorDiv) errorDiv.classList.add('hidden');
const formData = new FormData(this);
try {
const response = await fetch('/api/projects/create', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
const projectId = result.project_id;
// Add selected modules
const moduleMap = { module_sound: 'sound_monitoring', module_vibration: 'vibration_monitoring' };
for (const [field, moduleType] of Object.entries(moduleMap)) {
if (formData.get(field)) {
await fetch(`/api/projects/${projectId}/modules`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ module_type: moduleType }),
});
}
}
// Build display text from form values
const parts = [];
const projectNumber = formData.get('project_number');
const clientName = formData.get('client_name');
const projectName = formData.get('name');
if (projectNumber) parts.push(projectNumber);
if (clientName) parts.push(clientName);
if (projectName) parts.push(projectName);
const displayText = parts.join(' - ');
// Select the newly created project in the picker
selectProject(projectId, displayText, pickerId);
// Close modal
closeCreateProjectModal();
} else {
// Show error
if (errorDiv) {
errorDiv.textContent = result.detail || result.message || 'Failed to create project';
errorDiv.classList.remove('hidden');
}
}
} catch (error) {
if (errorDiv) {
errorDiv.textContent = `Error: ${error.message}`;
errorDiv.classList.remove('hidden');
}
} finally {
// Restore button
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
}
});
// Close modal on backdrop click
document.getElementById('quickCreateProjectModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeCreateProjectModal();
}
});
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('quickCreateProjectModal');
if (modal && !modal.classList.contains('hidden')) {
closeCreateProjectModal();
}
}
});
}
</script>