Refactor project dashboard and device list templates; add modals for editing projects and locations
- Updated project_dashboard.html to conditionally display NRLs or Locations based on project type, and added a button to open a modal for adding locations. - Enhanced slm_device_list.html with a configuration button for each unit, allowing users to open a modal for device configuration. - Modified detail.html to include an edit project modal with a form for updating project details, including client name, status, and dates. - Improved sound_level_meters.html by restructuring the layout and adding a configuration modal for SLM devices. - Implemented JavaScript functions for handling modal interactions, including opening, closing, and submitting forms for project and location management.
This commit is contained in:
@@ -8,7 +8,12 @@
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Project Dashboard</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Sound monitoring project overview and assignments</p>
|
||||
</div>
|
||||
<a href="/projects" class="text-sm text-seismo-orange hover:text-seismo-navy">Back to projects</a>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="openProjectEditModal()" class="px-4 py-2 text-sm bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg">
|
||||
Edit Project
|
||||
</button>
|
||||
<a href="/projects" class="text-sm text-seismo-orange hover:text-seismo-navy">Back to projects</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="project-dashboard"
|
||||
@@ -24,4 +29,534 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Project Modal -->
|
||||
<div id="project-edit-modal" 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-3xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Edit Project</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Update project details and status</p>
|
||||
</div>
|
||||
<button onclick="closeProjectEditModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<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>
|
||||
|
||||
<form id="project-edit-form" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project Name</label>
|
||||
<input type="text" name="name" id="edit-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" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
|
||||
<textarea name="description" id="edit-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>
|
||||
</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" id="edit-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">Status</label>
|
||||
<select name="status" id="edit-status" 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">
|
||||
<option value="active">Active</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</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" id="edit-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>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Site Coordinates</label>
|
||||
<input type="text" name="site_coordinates" id="edit-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>
|
||||
</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" id="edit-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</label>
|
||||
<input type="date" name="end_date" id="edit-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 id="edit-error" class="hidden text-sm text-red-600"></div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onclick="closeProjectEditModal()" 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">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Modal -->
|
||||
<div id="location-modal" 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-3xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 id="location-modal-title" class="text-2xl font-bold text-gray-900 dark:text-white">Add Location</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Create or update a monitoring location</p>
|
||||
</div>
|
||||
<button onclick="closeLocationModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<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>
|
||||
|
||||
<form id="location-form" class="p-6 space-y-4">
|
||||
<input type="hidden" id="location-id">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
|
||||
<input type="text" name="name" id="location-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" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
|
||||
<textarea name="description" id="location-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>
|
||||
</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">Type</label>
|
||||
<select name="location_type" id="location-type" 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">
|
||||
<option value="sound">Sound</option>
|
||||
<option value="vibration">Vibration</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Coordinates</label>
|
||||
<input type="text" name="coordinates" id="location-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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address</label>
|
||||
<input type="text" name="address" id="location-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 id="location-error" class="hidden text-sm text-red-600"></div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onclick="closeLocationModal()" 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">
|
||||
Save Location
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assign Unit Modal -->
|
||||
<div id="assign-modal" 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-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Assign Unit</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Attach a device to this location</p>
|
||||
</div>
|
||||
<button onclick="closeAssignModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<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>
|
||||
|
||||
<form id="assign-form" class="p-6 space-y-4">
|
||||
<input type="hidden" id="assign-location-id">
|
||||
<input type="hidden" id="assign-location-type">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Available Units</label>
|
||||
<select id="assign-unit-id" name="unit_id" 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" required>
|
||||
<option value="">Select a unit</option>
|
||||
</select>
|
||||
<p id="assign-empty" class="hidden text-xs text-gray-500 mt-2">No available units match this location type.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
|
||||
<textarea id="assign-notes" name="notes" 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"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="assign-error" class="hidden text-sm text-red-600"></div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onclick="closeAssignModal()" 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">
|
||||
Assign Unit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const projectId = "{{ project_id }}";
|
||||
let editingLocationId = null;
|
||||
let projectTypeId = null;
|
||||
|
||||
function openProjectEditModal() {
|
||||
const modal = document.getElementById('project-edit-modal');
|
||||
modal.classList.remove('hidden');
|
||||
loadProjectDetails();
|
||||
}
|
||||
|
||||
function closeProjectEditModal() {
|
||||
document.getElementById('project-edit-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function loadProjectDetails() {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load project details');
|
||||
}
|
||||
const data = await response.json();
|
||||
projectTypeId = data.project_type_id || null;
|
||||
document.getElementById('edit-name').value = data.name || '';
|
||||
document.getElementById('edit-description').value = data.description || '';
|
||||
document.getElementById('edit-client-name').value = data.client_name || '';
|
||||
document.getElementById('edit-status').value = data.status || 'active';
|
||||
document.getElementById('edit-site-address').value = data.site_address || '';
|
||||
document.getElementById('edit-site-coordinates').value = data.site_coordinates || '';
|
||||
document.getElementById('edit-start-date').value = formatDate(data.start_date);
|
||||
document.getElementById('edit-end-date').value = formatDate(data.end_date);
|
||||
document.getElementById('edit-error').classList.add('hidden');
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('edit-error');
|
||||
errorEl.textContent = err.message || 'Failed to load project details.';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectType() {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
projectTypeId = data.project_type_id || null;
|
||||
}
|
||||
} catch (err) {
|
||||
projectTypeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('project-edit-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
name: document.getElementById('edit-name').value.trim(),
|
||||
description: document.getElementById('edit-description').value.trim() || null,
|
||||
client_name: document.getElementById('edit-client-name').value.trim() || null,
|
||||
status: document.getElementById('edit-status').value,
|
||||
site_address: document.getElementById('edit-site-address').value.trim() || null,
|
||||
site_coordinates: document.getElementById('edit-site-coordinates').value.trim() || null,
|
||||
start_date: document.getElementById('edit-start-date').value || null,
|
||||
end_date: document.getElementById('edit-end-date').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update project');
|
||||
}
|
||||
|
||||
closeProjectEditModal();
|
||||
refreshProjectDashboard();
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('edit-error');
|
||||
errorEl.textContent = err.message || 'Failed to update project.';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function refreshProjectDashboard() {
|
||||
htmx.ajax('GET', `/api/projects/${projectId}/dashboard`, {
|
||||
target: '#project-dashboard',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
function openLocationModal(defaultType) {
|
||||
editingLocationId = null;
|
||||
document.getElementById('location-modal-title').textContent = 'Add Location';
|
||||
document.getElementById('location-id').value = '';
|
||||
document.getElementById('location-name').value = '';
|
||||
document.getElementById('location-description').value = '';
|
||||
document.getElementById('location-address').value = '';
|
||||
document.getElementById('location-coordinates').value = '';
|
||||
const locationTypeSelect = document.getElementById('location-type');
|
||||
const locationTypeWrapper = locationTypeSelect.closest('div');
|
||||
if (projectTypeId === 'sound_monitoring') {
|
||||
locationTypeSelect.value = 'sound';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
} else {
|
||||
locationTypeSelect.disabled = false;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.remove('hidden');
|
||||
locationTypeSelect.value = defaultType || 'sound';
|
||||
}
|
||||
document.getElementById('location-error').classList.add('hidden');
|
||||
document.getElementById('location-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openEditLocationModal(button) {
|
||||
const data = JSON.parse(button.dataset.location);
|
||||
editingLocationId = data.id;
|
||||
document.getElementById('location-modal-title').textContent = 'Edit Location';
|
||||
document.getElementById('location-id').value = data.id;
|
||||
document.getElementById('location-name').value = data.name || '';
|
||||
document.getElementById('location-description').value = data.description || '';
|
||||
document.getElementById('location-address').value = data.address || '';
|
||||
document.getElementById('location-coordinates').value = data.coordinates || '';
|
||||
const locationTypeSelect = document.getElementById('location-type');
|
||||
const locationTypeWrapper = locationTypeSelect.closest('div');
|
||||
if (projectTypeId === 'sound_monitoring') {
|
||||
locationTypeSelect.value = 'sound';
|
||||
locationTypeSelect.disabled = true;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.add('hidden');
|
||||
} else {
|
||||
locationTypeSelect.disabled = false;
|
||||
if (locationTypeWrapper) locationTypeWrapper.classList.remove('hidden');
|
||||
locationTypeSelect.value = data.location_type || 'sound';
|
||||
}
|
||||
document.getElementById('location-error').classList.add('hidden');
|
||||
document.getElementById('location-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeLocationModal() {
|
||||
document.getElementById('location-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('location-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('location-name').value.trim();
|
||||
const description = document.getElementById('location-description').value.trim();
|
||||
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') {
|
||||
locationType = 'sound';
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingLocationId) {
|
||||
const payload = {
|
||||
name,
|
||||
description: description || null,
|
||||
address: address || null,
|
||||
coordinates: coordinates || null,
|
||||
location_type: locationType
|
||||
};
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/${editingLocationId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to update location');
|
||||
}
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('description', description);
|
||||
formData.append('address', address);
|
||||
formData.append('coordinates', coordinates);
|
||||
formData.append('location_type', locationType);
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/create`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to create location');
|
||||
}
|
||||
}
|
||||
|
||||
closeLocationModal();
|
||||
refreshProjectDashboard();
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('location-error');
|
||||
errorEl.textContent = err.message || 'Failed to save location.';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteLocation(locationId) {
|
||||
if (!confirm('Delete this location?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to delete location');
|
||||
}
|
||||
refreshProjectDashboard();
|
||||
} catch (err) {
|
||||
alert(err.message || 'Failed to delete location.');
|
||||
}
|
||||
}
|
||||
|
||||
function openAssignModal(locationId, locationType) {
|
||||
const safeType = locationType || 'sound';
|
||||
document.getElementById('assign-location-id').value = locationId;
|
||||
document.getElementById('assign-location-type').value = safeType;
|
||||
document.getElementById('assign-unit-id').innerHTML = '<option value=\"\">Loading units...</option>';
|
||||
document.getElementById('assign-empty').classList.add('hidden');
|
||||
document.getElementById('assign-error').classList.add('hidden');
|
||||
document.getElementById('assign-modal').classList.remove('hidden');
|
||||
loadAvailableUnits(safeType);
|
||||
}
|
||||
|
||||
function closeAssignModal() {
|
||||
document.getElementById('assign-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function loadAvailableUnits(locationType) {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/available-units?location_type=${locationType}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load available units');
|
||||
}
|
||||
const data = await response.json();
|
||||
const select = document.getElementById('assign-unit-id');
|
||||
select.innerHTML = '<option value=\"\">Select a unit</option>';
|
||||
if (!data.length) {
|
||||
document.getElementById('assign-empty').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
data.forEach(unit => {
|
||||
const option = document.createElement('option');
|
||||
option.value = unit.id;
|
||||
option.textContent = `${unit.id} • ${unit.model || unit.device_type}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('assign-error');
|
||||
errorEl.textContent = err.message || 'Failed to load units.';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('assign-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const locationId = document.getElementById('assign-location-id').value;
|
||||
const unitId = document.getElementById('assign-unit-id').value;
|
||||
const notes = document.getElementById('assign-notes').value.trim();
|
||||
|
||||
if (!unitId) {
|
||||
document.getElementById('assign-error').textContent = 'Select a unit to assign.';
|
||||
document.getElementById('assign-error').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('unit_id', unitId);
|
||||
formData.append('notes', notes);
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}/assign`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to assign unit');
|
||||
}
|
||||
closeAssignModal();
|
||||
refreshProjectDashboard();
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('assign-error');
|
||||
errorEl.textContent = err.message || 'Failed to assign unit.';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
async function unassignUnit(assignmentId) {
|
||||
if (!confirm('Unassign this unit?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/assignments/${assignmentId}/unassign`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to unassign unit');
|
||||
}
|
||||
refreshProjectDashboard();
|
||||
} catch (err) {
|
||||
alert(err.message || 'Failed to unassign unit.');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeProjectEditModal();
|
||||
closeLocationModal();
|
||||
closeAssignModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadProjectType();
|
||||
});
|
||||
|
||||
document.getElementById('project-edit-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeProjectEditModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('location-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeLocationModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('assign-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeAssignModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user