feat: Add report templates API for CRUD operations and implement SLM settings modal
- Implemented a new API router for managing report templates, including endpoints for listing, creating, retrieving, updating, and deleting templates. - Added a new HTML partial for a unified SLM settings modal, allowing users to configure SLM settings with dynamic modem selection and FTP credentials. - Created a report preview page with an editable data table using jspreadsheet, enabling users to modify report details and download the report as an Excel file.
This commit is contained in:
@@ -22,6 +22,16 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="showFTPSettings('{{ unit_item.unit.id }}')"
|
||||
id="settings-ftp-{{ unit_item.unit.id }}"
|
||||
class="px-3 py-1 text-xs bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-1"
|
||||
title="Configure FTP credentials">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
Settings
|
||||
</button>
|
||||
<button onclick="enableFTP('{{ unit_item.unit.id }}')"
|
||||
id="enable-ftp-{{ unit_item.unit.id }}"
|
||||
class="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
@@ -605,3 +615,6 @@ setTimeout(function() {
|
||||
{% endfor %}
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<!-- Include the unified SLM Settings Modal -->
|
||||
{% include 'partials/slm_settings_modal.html' %}
|
||||
|
||||
@@ -1307,123 +1307,6 @@ window.addEventListener('beforeunload', function() {
|
||||
// Timer will resume on next page load if measurement is still active
|
||||
stopMeasurementTimer();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Settings Modal
|
||||
// ========================================
|
||||
async function openSettingsModal(unitId) {
|
||||
const modal = document.getElementById('settings-modal');
|
||||
const errorDiv = document.getElementById('settings-error');
|
||||
const successDiv = document.getElementById('settings-success');
|
||||
|
||||
// Clear previous messages
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Store unit ID
|
||||
document.getElementById('settings-unit-id').value = unitId;
|
||||
|
||||
// Load current SLMM config
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load configuration');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const config = result.data || {};
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById('settings-host').value = config.host || '';
|
||||
document.getElementById('settings-tcp-port').value = config.tcp_port || 2255;
|
||||
document.getElementById('settings-ftp-port').value = config.ftp_port || 21;
|
||||
document.getElementById('settings-ftp-username').value = config.ftp_username || '';
|
||||
document.getElementById('settings-ftp-password').value = config.ftp_password || '';
|
||||
document.getElementById('settings-tcp-enabled').checked = config.tcp_enabled !== false;
|
||||
document.getElementById('settings-ftp-enabled').checked = config.ftp_enabled === true;
|
||||
document.getElementById('settings-web-enabled').checked = config.web_enabled === true;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Failed to load SLMM config:', error);
|
||||
errorDiv.textContent = 'Failed to load configuration: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeSettingsModal() {
|
||||
document.getElementById('settings-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('settings-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const unitId = document.getElementById('settings-unit-id').value;
|
||||
const errorDiv = document.getElementById('settings-error');
|
||||
const successDiv = document.getElementById('settings-success');
|
||||
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Gather form data
|
||||
const configData = {
|
||||
host: document.getElementById('settings-host').value.trim(),
|
||||
tcp_port: parseInt(document.getElementById('settings-tcp-port').value),
|
||||
ftp_port: parseInt(document.getElementById('settings-ftp-port').value),
|
||||
ftp_username: document.getElementById('settings-ftp-username').value.trim() || null,
|
||||
ftp_password: document.getElementById('settings-ftp-password').value || null,
|
||||
tcp_enabled: document.getElementById('settings-tcp-enabled').checked,
|
||||
ftp_enabled: document.getElementById('settings-ftp-enabled').checked,
|
||||
web_enabled: document.getElementById('settings-web-enabled').checked
|
||||
};
|
||||
|
||||
// Validation
|
||||
if (!configData.host) {
|
||||
errorDiv.textContent = 'Host/IP address is required';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (configData.tcp_port < 1 || configData.tcp_port > 65535) {
|
||||
errorDiv.textContent = 'TCP port must be between 1 and 65535';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (configData.ftp_port < 1 || configData.ftp_port > 65535) {
|
||||
errorDiv.textContent = 'FTP port must be between 1 and 65535';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/config`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(configData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to update configuration');
|
||||
}
|
||||
|
||||
successDiv.textContent = 'Configuration saved successfully!';
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
// Close modal after 1.5 seconds
|
||||
setTimeout(() => {
|
||||
closeSettingsModal();
|
||||
// Optionally reload the page to reflect changes
|
||||
// window.location.reload();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// FTP Browser Modal
|
||||
// ========================================
|
||||
@@ -2201,125 +2084,6 @@ document.getElementById('preview-modal')?.addEventListener('click', function(e)
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-2xl m-4 my-8">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">SLM Configuration</h3>
|
||||
<button onclick="closeSettingsModal()" 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="settings-form" class="p-6 space-y-6">
|
||||
<input type="hidden" id="settings-unit-id">
|
||||
|
||||
<!-- Network Configuration -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Network Configuration</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Host / IP Address</label>
|
||||
<input type="text" id="settings-host"
|
||||
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"
|
||||
placeholder="e.g., 192.168.1.100" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">TCP Port</label>
|
||||
<input type="number" id="settings-tcp-port"
|
||||
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"
|
||||
placeholder="2255" min="1" max="65535" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Default: 2255 for NL-43/NL-53</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">FTP Port</label>
|
||||
<input type="number" id="settings-ftp-port"
|
||||
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"
|
||||
placeholder="21" min="1" max="65535" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Standard FTP port (default: 21)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Credentials -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">FTP Credentials</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Username</label>
|
||||
<input type="text" id="settings-ftp-username"
|
||||
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"
|
||||
placeholder="anonymous">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank for anonymous</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="settings-ftp-password"
|
||||
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"
|
||||
placeholder="••••••••">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank for anonymous</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol Toggles -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Protocol Settings</h4>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">TCP Communication</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable TCP control commands</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-tcp-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">FTP File Transfer</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable FTP file browsing and downloads</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-ftp-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Web Interface</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable web UI access (future feature)</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-web-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-error" class="hidden text-sm p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg"></div>
|
||||
<div id="settings-success" class="hidden text-sm p-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-lg"></div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button type="button" onclick="closeSettingsModal()"
|
||||
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-orange-600 text-white rounded-lg font-medium">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Browser Modal -->
|
||||
<div id="ftp-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-4xl max-h-[90vh] overflow-hidden m-4 flex flex-col">
|
||||
@@ -2407,3 +2171,6 @@ document.getElementById('preview-modal')?.addEventListener('click', function(e)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unified SLM Settings Modal -->
|
||||
{% include 'partials/slm_settings_modal.html' %}
|
||||
|
||||
534
templates/partials/slm_settings_modal.html
Normal file
534
templates/partials/slm_settings_modal.html
Normal file
@@ -0,0 +1,534 @@
|
||||
<!-- Unified SLM Settings Modal - Include this partial where SLM settings are needed -->
|
||||
<!-- Usage: include 'partials/slm_settings_modal.html' (with Jinja braces) -->
|
||||
<!-- Then call: openSLMSettingsModal(unitId) from JavaScript -->
|
||||
|
||||
<div id="slm-settings-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-2xl m-4 my-8">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-8 h-8 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">SLM Configuration</h3>
|
||||
<p id="slm-settings-unit-display" class="text-sm text-gray-500 dark:text-gray-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeSLMSettingsModal()" 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="slm-settings-form" onsubmit="saveSLMSettings(event)" class="p-6 space-y-6">
|
||||
<input type="hidden" id="slm-settings-unit-id">
|
||||
|
||||
<!-- Network Configuration -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path>
|
||||
</svg>
|
||||
Network Configuration
|
||||
</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Modem Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Connected via Modem</label>
|
||||
<div class="flex gap-2">
|
||||
<select id="slm-settings-modem" class="flex-1 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="">Select a modem...</option>
|
||||
<!-- Modems loaded dynamically -->
|
||||
</select>
|
||||
<button type="button" onclick="testModemConnection()" id="slm-settings-test-modem-btn"
|
||||
class="px-4 py-2 text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled title="Test modem connectivity">
|
||||
<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="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Select the modem this SLM is connected through</p>
|
||||
</div>
|
||||
|
||||
<!-- Port Configuration -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">TCP Port</label>
|
||||
<input type="number" id="slm-settings-tcp-port"
|
||||
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"
|
||||
placeholder="2255" min="1" max="65535" value="2255">
|
||||
<p class="text-xs text-gray-500 mt-1">Control port (default: 2255)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">FTP Port</label>
|
||||
<input type="number" id="slm-settings-ftp-port"
|
||||
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"
|
||||
placeholder="21" min="1" max="65535" value="21">
|
||||
<p class="text-xs text-gray-500 mt-1">File transfer (default: 21)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Credentials -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
FTP Credentials
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Username</label>
|
||||
<input type="text" id="slm-settings-ftp-username"
|
||||
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"
|
||||
placeholder="anonymous">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank for anonymous</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="slm-settings-ftp-password"
|
||||
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"
|
||||
placeholder="Leave blank to keep existing">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank to keep existing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Information -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
|
||||
</svg>
|
||||
Device Information
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Model</label>
|
||||
<select id="slm-settings-model" 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="">Select model...</option>
|
||||
<option value="NL-43">NL-43</option>
|
||||
<option value="NL-53">NL-53</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Serial Number</label>
|
||||
<input type="text" id="slm-settings-serial"
|
||||
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"
|
||||
placeholder="e.g., SN123456">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 mt-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Frequency Weighting</label>
|
||||
<select id="slm-settings-freq-weighting" 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="">Select...</option>
|
||||
<option value="A">A-weighting</option>
|
||||
<option value="C">C-weighting</option>
|
||||
<option value="Z">Z-weighting (Linear)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Weighting</label>
|
||||
<select id="slm-settings-time-weighting" 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="">Select...</option>
|
||||
<option value="Fast">Fast (125ms)</option>
|
||||
<option value="Slow">Slow (1s)</option>
|
||||
<option value="Impulse">Impulse</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Measurement Range</label>
|
||||
<select id="slm-settings-range" 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="">Select...</option>
|
||||
<option value="30-130">30-130 dB</option>
|
||||
<option value="40-140">40-140 dB</option>
|
||||
<option value="50-140">50-140 dB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Enable Toggle -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<label class="flex items-center justify-between cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">FTP File Transfer</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Enable FTP for file browsing and downloads</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" id="slm-settings-ftp-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="slm-settings-error" class="hidden text-sm p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg"></div>
|
||||
<div id="slm-settings-success" class="hidden text-sm p-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-lg"></div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button type="button" onclick="closeSLMSettingsModal()"
|
||||
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="button" onclick="testSLMConnection()"
|
||||
class="px-6 py-2 text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 rounded-lg">
|
||||
Test SLM Connection
|
||||
</button>
|
||||
<button type="submit" id="slm-settings-save-btn"
|
||||
class="px-6 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg font-medium">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ========================================
|
||||
// Unified SLM Settings Modal JavaScript
|
||||
// ========================================
|
||||
|
||||
let slmSettingsModems = []; // Cache modems list
|
||||
|
||||
// Open the SLM Settings Modal
|
||||
async function openSLMSettingsModal(unitId) {
|
||||
const modal = document.getElementById('slm-settings-modal');
|
||||
const errorDiv = document.getElementById('slm-settings-error');
|
||||
const successDiv = document.getElementById('slm-settings-success');
|
||||
|
||||
// Clear previous messages
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Store unit ID
|
||||
document.getElementById('slm-settings-unit-id').value = unitId;
|
||||
document.getElementById('slm-settings-unit-display').textContent = unitId;
|
||||
|
||||
// Load modems list if not cached
|
||||
if (slmSettingsModems.length === 0) {
|
||||
await loadModemsForSLMSettings();
|
||||
}
|
||||
|
||||
// Load current config from both Terra-View and SLMM
|
||||
try {
|
||||
// Fetch Terra-View unit data
|
||||
const unitResponse = await fetch(`/api/roster/${unitId}`);
|
||||
const unitData = unitResponse.ok ? await unitResponse.json() : {};
|
||||
|
||||
// Fetch SLMM config
|
||||
const slmmResponse = await fetch(`/api/slmm/${unitId}/config`);
|
||||
const slmmResult = slmmResponse.ok ? await slmmResponse.json() : {};
|
||||
const slmmData = slmmResult.data || slmmResult || {};
|
||||
|
||||
// Populate form fields
|
||||
// Modem selection
|
||||
const modemSelect = document.getElementById('slm-settings-modem');
|
||||
modemSelect.value = unitData.deployed_with_modem_id || '';
|
||||
updateTestModemButton();
|
||||
|
||||
// Ports
|
||||
document.getElementById('slm-settings-tcp-port').value = unitData.slm_tcp_port || slmmData.tcp_port || 2255;
|
||||
document.getElementById('slm-settings-ftp-port').value = unitData.slm_ftp_port || slmmData.ftp_port || 21;
|
||||
|
||||
// FTP credentials from SLMM
|
||||
document.getElementById('slm-settings-ftp-username').value = slmmData.ftp_username || '';
|
||||
document.getElementById('slm-settings-ftp-password').value = ''; // Don't pre-fill
|
||||
|
||||
// Device info from Terra-View
|
||||
document.getElementById('slm-settings-model').value = unitData.slm_model || '';
|
||||
document.getElementById('slm-settings-serial').value = unitData.slm_serial_number || '';
|
||||
document.getElementById('slm-settings-freq-weighting').value = unitData.slm_frequency_weighting || '';
|
||||
document.getElementById('slm-settings-time-weighting').value = unitData.slm_time_weighting || '';
|
||||
document.getElementById('slm-settings-range').value = unitData.slm_measurement_range || '';
|
||||
|
||||
// FTP enabled from SLMM
|
||||
document.getElementById('slm-settings-ftp-enabled').checked = slmmData.ftp_enabled === true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load SLM settings:', error);
|
||||
errorDiv.textContent = 'Failed to load configuration: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
function closeSLMSettingsModal() {
|
||||
document.getElementById('slm-settings-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility with existing code
|
||||
function showFTPSettings(unitId) {
|
||||
openSLMSettingsModal(unitId);
|
||||
}
|
||||
function closeFTPSettings() {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
function openSettingsModal(unitId) {
|
||||
openSLMSettingsModal(unitId);
|
||||
}
|
||||
function closeSettingsModal() {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
function openConfigModal(unitId) {
|
||||
openSLMSettingsModal(unitId);
|
||||
}
|
||||
function closeConfigModal() {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
function openDeviceConfigModal(unitId) {
|
||||
openSLMSettingsModal(unitId);
|
||||
}
|
||||
function closeDeviceConfigModal() {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
|
||||
// Load modems for dropdown
|
||||
async function loadModemsForSLMSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/roster/modems');
|
||||
slmSettingsModems = await response.json();
|
||||
|
||||
const select = document.getElementById('slm-settings-modem');
|
||||
// Clear existing options except first
|
||||
select.innerHTML = '<option value="">Select a modem...</option>';
|
||||
|
||||
slmSettingsModems.forEach(modem => {
|
||||
const option = document.createElement('option');
|
||||
option.value = modem.id;
|
||||
const ipText = modem.ip_address ? ` (${modem.ip_address})` : '';
|
||||
const deployedText = modem.deployed ? '' : ' [Benched]';
|
||||
option.textContent = modem.id + ipText + deployedText;
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load modems:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update test modem button state based on selection
|
||||
function updateTestModemButton() {
|
||||
const modemSelect = document.getElementById('slm-settings-modem');
|
||||
const testBtn = document.getElementById('slm-settings-test-modem-btn');
|
||||
testBtn.disabled = !modemSelect.value;
|
||||
}
|
||||
|
||||
// Listen for modem selection changes
|
||||
document.getElementById('slm-settings-modem')?.addEventListener('change', updateTestModemButton);
|
||||
|
||||
// Test modem connection
|
||||
async function testModemConnection() {
|
||||
const modemId = document.getElementById('slm-settings-modem').value;
|
||||
if (!modemId) return;
|
||||
|
||||
const errorDiv = document.getElementById('slm-settings-error');
|
||||
const successDiv = document.getElementById('slm-settings-success');
|
||||
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.textContent = 'Pinging modem...';
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slm-dashboard/test-modem/${modemId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === 'success') {
|
||||
const ipAddr = data.ip_address || modemId;
|
||||
const respTime = data.response_time || 'N/A';
|
||||
successDiv.textContent = `✓ Modem responding! ${ipAddr} - ${respTime}ms`;
|
||||
} else {
|
||||
successDiv.classList.add('hidden');
|
||||
errorDiv.textContent = '⚠ Modem not responding: ' + (data.detail || 'Unknown error');
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
successDiv.classList.add('hidden');
|
||||
errorDiv.textContent = 'Failed to ping modem: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Test SLM connection
|
||||
async function testSLMConnection() {
|
||||
const unitId = document.getElementById('slm-settings-unit-id').value;
|
||||
const errorDiv = document.getElementById('slm-settings-error');
|
||||
const successDiv = document.getElementById('slm-settings-success');
|
||||
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.textContent = 'Testing SLM connection...';
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/status`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === 'online') {
|
||||
successDiv.textContent = '✓ SLM connection successful! Device is responding.';
|
||||
} else {
|
||||
successDiv.classList.add('hidden');
|
||||
errorDiv.textContent = '⚠ SLM not responding or offline. Check network settings.';
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
successDiv.classList.add('hidden');
|
||||
errorDiv.textContent = 'Connection test failed: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Save SLM settings
|
||||
async function saveSLMSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const unitId = document.getElementById('slm-settings-unit-id').value;
|
||||
const saveBtn = document.getElementById('slm-settings-save-btn');
|
||||
const errorDiv = document.getElementById('slm-settings-error');
|
||||
const successDiv = document.getElementById('slm-settings-success');
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Get selected modem and resolve its IP
|
||||
const modemId = document.getElementById('slm-settings-modem').value;
|
||||
let modemIp = '';
|
||||
if (modemId) {
|
||||
const modem = slmSettingsModems.find(m => m.id === modemId);
|
||||
modemIp = modem?.ip_address || '';
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (!modemId) {
|
||||
errorDiv.textContent = 'Please select a modem';
|
||||
errorDiv.classList.remove('hidden');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save Configuration';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modemIp) {
|
||||
errorDiv.textContent = 'Selected modem has no IP address configured';
|
||||
errorDiv.classList.remove('hidden');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save Configuration';
|
||||
return;
|
||||
}
|
||||
|
||||
const tcpPort = parseInt(document.getElementById('slm-settings-tcp-port').value) || 2255;
|
||||
const ftpPort = parseInt(document.getElementById('slm-settings-ftp-port').value) || 21;
|
||||
|
||||
if (tcpPort < 1 || tcpPort > 65535 || ftpPort < 1 || ftpPort > 65535) {
|
||||
errorDiv.textContent = 'Port values must be between 1 and 65535';
|
||||
errorDiv.classList.remove('hidden');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save Configuration';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Update Terra-View database (device info + modem assignment)
|
||||
const terraViewData = {
|
||||
deployed_with_modem_id: modemId,
|
||||
slm_model: document.getElementById('slm-settings-model').value || null,
|
||||
slm_serial_number: document.getElementById('slm-settings-serial').value || null,
|
||||
slm_frequency_weighting: document.getElementById('slm-settings-freq-weighting').value || null,
|
||||
slm_time_weighting: document.getElementById('slm-settings-time-weighting').value || null,
|
||||
slm_measurement_range: document.getElementById('slm-settings-range').value || null,
|
||||
slm_tcp_port: tcpPort,
|
||||
slm_ftp_port: ftpPort
|
||||
};
|
||||
|
||||
const terraResponse = await fetch(`/api/slm-dashboard/config/${unitId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(terraViewData)
|
||||
});
|
||||
|
||||
if (!terraResponse.ok) {
|
||||
throw new Error('Failed to save Terra-View configuration');
|
||||
}
|
||||
|
||||
// 2. Update SLMM config (network + FTP credentials)
|
||||
const slmmData = {
|
||||
host: modemIp,
|
||||
tcp_port: tcpPort,
|
||||
ftp_port: ftpPort,
|
||||
ftp_username: document.getElementById('slm-settings-ftp-username').value.trim() || null,
|
||||
ftp_enabled: document.getElementById('slm-settings-ftp-enabled').checked
|
||||
};
|
||||
|
||||
// Only include password if entered
|
||||
const password = document.getElementById('slm-settings-ftp-password').value;
|
||||
if (password) {
|
||||
slmmData.ftp_password = password;
|
||||
}
|
||||
|
||||
const slmmResponse = await fetch(`/api/slmm/${unitId}/config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(slmmData)
|
||||
});
|
||||
|
||||
if (!slmmResponse.ok) {
|
||||
const errData = await slmmResponse.json().catch(() => ({}));
|
||||
throw new Error(errData.detail || 'Failed to save SLMM configuration');
|
||||
}
|
||||
|
||||
successDiv.textContent = 'Configuration saved successfully!';
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
// Close modal after delay and refresh if needed
|
||||
setTimeout(() => {
|
||||
closeSLMSettingsModal();
|
||||
// Try to refresh any FTP status or unit lists on the page
|
||||
if (typeof checkFTPStatus === 'function') {
|
||||
checkFTPStatus(unitId);
|
||||
}
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.trigger('#slm-list', 'load');
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
errorDiv.textContent = 'Error: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save Configuration';
|
||||
}
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility
|
||||
async function saveFTPSettings(event) {
|
||||
return saveSLMSettings(event);
|
||||
}
|
||||
|
||||
// Close modal on background click
|
||||
document.getElementById('slm-settings-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
309
templates/report_preview.html
Normal file
309
templates/report_preview.html
Normal file
@@ -0,0 +1,309 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Report Preview - {{ project.name if project else 'Sound Level Data' }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- jspreadsheet CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/jspreadsheet.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsuites@5/dist/jsuites.min.css" />
|
||||
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-slate-900">
|
||||
<!-- Header -->
|
||||
<div class="bg-white dark:bg-slate-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Report Preview & Editor
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{% if file %}{{ file.file_path.split('/')[-1] }}{% endif %}
|
||||
{% if location %} @ {{ location.name }}{% endif %}
|
||||
{% if start_time and end_time %} | Time: {{ start_time }} - {{ end_time }}{% endif %}
|
||||
| {{ filtered_count }} of {{ original_count }} rows
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="downloadReport()"
|
||||
class="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors flex items-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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Download Excel
|
||||
</button>
|
||||
<a href="/api/projects/{{ project_id }}/files/{{ file_id }}/view-rnd"
|
||||
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
Back to Viewer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Info Section -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Report Title</label>
|
||||
<input type="text" id="edit-report-title" value="{{ report_title }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
|
||||
<input type="text" id="edit-project-name" value="{{ project_name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Client Name</label>
|
||||
<input type="text" id="edit-client-name" value="{{ client_name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Location</label>
|
||||
<input type="text" id="edit-location-name" value="{{ location_name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spreadsheet Editor -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Data Table</h2>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>Right-click for options</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<span>Double-click to edit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="spreadsheet" class="overflow-x-auto"></div>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div class="mt-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-300 mb-2">Editing Tips</h3>
|
||||
<ul class="text-sm text-blue-700 dark:text-blue-400 list-disc list-inside space-y-1">
|
||||
<li>Double-click any cell to edit its value</li>
|
||||
<li>Use the Comments column to add notes about specific measurements</li>
|
||||
<li>Right-click a row to delete it from the report</li>
|
||||
<li>Right-click to add new rows if needed</li>
|
||||
<li>Press Enter to confirm edits, Escape to cancel</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jspreadsheet JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsuites@5/dist/jsuites.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/index.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Initialize spreadsheet data from server
|
||||
const initialData = {{ spreadsheet_data | tojson }};
|
||||
|
||||
// Create jspreadsheet instance
|
||||
let spreadsheet = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
|
||||
data: initialData,
|
||||
columns: [
|
||||
{ title: 'Test #', width: 80, type: 'numeric' },
|
||||
{ title: 'Date', width: 110, type: 'text' },
|
||||
{ title: 'Time', width: 90, type: 'text' },
|
||||
{ title: 'LAmax (dBA)', width: 100, type: 'numeric' },
|
||||
{ title: 'LA01 (dBA)', width: 100, type: 'numeric' },
|
||||
{ title: 'LA10 (dBA)', width: 100, type: 'numeric' },
|
||||
{ title: 'Comments', width: 250, type: 'text' }
|
||||
],
|
||||
allowInsertRow: true,
|
||||
allowDeleteRow: true,
|
||||
allowInsertColumn: false,
|
||||
allowDeleteColumn: false,
|
||||
rowDrag: true,
|
||||
columnSorting: true,
|
||||
search: true,
|
||||
pagination: 50,
|
||||
paginationOptions: [25, 50, 100, 200],
|
||||
defaultColWidth: 100,
|
||||
minDimensions: [7, 1],
|
||||
tableOverflow: true,
|
||||
tableWidth: '100%',
|
||||
contextMenu: function(instance, col, row, e) {
|
||||
const items = [];
|
||||
|
||||
if (row !== null) {
|
||||
items.push({
|
||||
title: 'Insert row above',
|
||||
onclick: function() {
|
||||
instance.insertRow(1, row, true);
|
||||
}
|
||||
});
|
||||
items.push({
|
||||
title: 'Insert row below',
|
||||
onclick: function() {
|
||||
instance.insertRow(1, row + 1, false);
|
||||
}
|
||||
});
|
||||
items.push({
|
||||
title: 'Delete this row',
|
||||
onclick: function() {
|
||||
instance.deleteRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
style: {
|
||||
A: 'text-align: center;',
|
||||
B: 'text-align: center;',
|
||||
C: 'text-align: center;',
|
||||
D: 'text-align: right;',
|
||||
E: 'text-align: right;',
|
||||
F: 'text-align: right;',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function downloadReport() {
|
||||
// Get current data from spreadsheet
|
||||
const data = spreadsheet.getData();
|
||||
|
||||
// Get report settings
|
||||
const reportTitle = document.getElementById('edit-report-title').value;
|
||||
const projectName = document.getElementById('edit-project-name').value;
|
||||
const clientName = document.getElementById('edit-client-name').value;
|
||||
const locationName = document.getElementById('edit-location-name').value;
|
||||
|
||||
// Build time filter info
|
||||
let timeFilter = '';
|
||||
{% if start_time and end_time %}
|
||||
timeFilter = 'Time Filter: {{ start_time }} - {{ end_time }}';
|
||||
{% if start_date or end_date %}
|
||||
timeFilter += ' | Date Range: {{ start_date or "start" }} to {{ end_date or "end" }}';
|
||||
{% endif %}
|
||||
timeFilter += ' | {{ filtered_count }} of {{ original_count }} rows';
|
||||
{% endif %}
|
||||
|
||||
// Send to server to generate Excel
|
||||
try {
|
||||
const response = await fetch('/api/projects/{{ project_id }}/files/{{ file_id }}/generate-from-preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: data,
|
||||
report_title: reportTitle,
|
||||
project_name: projectName,
|
||||
client_name: clientName,
|
||||
location_name: locationName,
|
||||
time_filter: timeFilter
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Download the file
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Get filename from Content-Disposition header if available
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'report.xlsx';
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="(.+)"/);
|
||||
if (match) filename = match[1];
|
||||
}
|
||||
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert('Error generating report: ' + (error.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error generating report: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Custom styles for jspreadsheet to match dark mode */
|
||||
.dark .jexcel {
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .jexcel thead td {
|
||||
background-color: #334155 !important;
|
||||
color: #e2e8f0 !important;
|
||||
border-color: #475569 !important;
|
||||
}
|
||||
|
||||
.dark .jexcel tbody td {
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.dark .jexcel tbody td:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
.dark .jexcel tbody tr:nth-child(even) td {
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
.dark .jexcel_pagination {
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.dark .jexcel_pagination a {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .jexcel_search {
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.dark .jexcel_search input {
|
||||
background-color: #334155;
|
||||
color: #e2e8f0;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.dark .jexcel_content {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
.dark .jexcel_contextmenu {
|
||||
background-color: #1e293b;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.dark .jexcel_contextmenu a {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .jexcel_contextmenu a:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
/* Ensure proper sizing */
|
||||
.jexcel_content {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -454,42 +454,227 @@ function escapeHtml(str) {
|
||||
}
|
||||
|
||||
// Report Generation Modal Functions
|
||||
let reportTemplates = [];
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const response = await fetch('/api/report-templates?project_id={{ project_id }}');
|
||||
if (response.ok) {
|
||||
reportTemplates = await response.json();
|
||||
populateTemplateDropdown();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateTemplateDropdown() {
|
||||
const select = document.getElementById('template-select');
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '<option value="">-- Select a template --</option>';
|
||||
reportTemplates.forEach(template => {
|
||||
const option = document.createElement('option');
|
||||
option.value = template.id;
|
||||
option.textContent = template.name;
|
||||
option.dataset.config = JSON.stringify(template);
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function applyTemplate() {
|
||||
const select = document.getElementById('template-select');
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
|
||||
if (!selectedOption.value) return;
|
||||
|
||||
const template = JSON.parse(selectedOption.dataset.config);
|
||||
|
||||
if (template.report_title) {
|
||||
document.getElementById('report-title').value = template.report_title;
|
||||
}
|
||||
if (template.start_time) {
|
||||
document.getElementById('start-time').value = template.start_time;
|
||||
}
|
||||
if (template.end_time) {
|
||||
document.getElementById('end-time').value = template.end_time;
|
||||
}
|
||||
if (template.start_date) {
|
||||
document.getElementById('start-date').value = template.start_date;
|
||||
}
|
||||
if (template.end_date) {
|
||||
document.getElementById('end-date').value = template.end_date;
|
||||
}
|
||||
|
||||
// Update preset buttons
|
||||
updatePresetButtons();
|
||||
}
|
||||
|
||||
function setTimePreset(preset) {
|
||||
const startTimeInput = document.getElementById('start-time');
|
||||
const endTimeInput = document.getElementById('end-time');
|
||||
|
||||
// Remove active state from all preset buttons
|
||||
document.querySelectorAll('.preset-btn').forEach(btn => {
|
||||
btn.classList.remove('bg-emerald-600', 'text-white');
|
||||
btn.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
|
||||
});
|
||||
|
||||
// Set time values based on preset
|
||||
switch(preset) {
|
||||
case 'night':
|
||||
startTimeInput.value = '19:00';
|
||||
endTimeInput.value = '07:00';
|
||||
break;
|
||||
case 'day':
|
||||
startTimeInput.value = '07:00';
|
||||
endTimeInput.value = '19:00';
|
||||
break;
|
||||
case 'all':
|
||||
startTimeInput.value = '';
|
||||
endTimeInput.value = '';
|
||||
break;
|
||||
case 'custom':
|
||||
// Just enable custom input, don't change values
|
||||
break;
|
||||
}
|
||||
|
||||
// Highlight active preset
|
||||
const activeBtn = document.querySelector(`[data-preset="${preset}"]`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
|
||||
activeBtn.classList.add('bg-emerald-600', 'text-white');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePresetButtons() {
|
||||
const startTime = document.getElementById('start-time').value;
|
||||
const endTime = document.getElementById('end-time').value;
|
||||
|
||||
// Remove active state from all
|
||||
document.querySelectorAll('.preset-btn').forEach(btn => {
|
||||
btn.classList.remove('bg-emerald-600', 'text-white');
|
||||
btn.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
|
||||
});
|
||||
|
||||
// Check which preset matches
|
||||
let preset = 'custom';
|
||||
if (startTime === '19:00' && endTime === '07:00') preset = 'night';
|
||||
else if (startTime === '07:00' && endTime === '19:00') preset = 'day';
|
||||
else if (!startTime && !endTime) preset = 'all';
|
||||
|
||||
const activeBtn = document.querySelector(`[data-preset="${preset}"]`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
|
||||
activeBtn.classList.add('bg-emerald-600', 'text-white');
|
||||
}
|
||||
}
|
||||
|
||||
function openReportModal() {
|
||||
document.getElementById('report-modal').classList.remove('hidden');
|
||||
// Pre-fill location name if available
|
||||
loadTemplates();
|
||||
|
||||
// Pre-fill fields if available
|
||||
const locationInput = document.getElementById('report-location');
|
||||
if (locationInput && !locationInput.value) {
|
||||
locationInput.value = '{{ location.name if location else "" }}';
|
||||
}
|
||||
const projectInput = document.getElementById('report-project');
|
||||
if (projectInput && !projectInput.value) {
|
||||
projectInput.value = '{{ project.name if project else "" }}';
|
||||
}
|
||||
const clientInput = document.getElementById('report-client');
|
||||
if (clientInput && !clientInput.value) {
|
||||
clientInput.value = '{{ project.client_name if project and project.client_name else "" }}';
|
||||
}
|
||||
|
||||
// Set default to "All Day"
|
||||
setTimePreset('all');
|
||||
}
|
||||
|
||||
function closeReportModal() {
|
||||
document.getElementById('report-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
function generateReport(preview = false) {
|
||||
const reportTitle = document.getElementById('report-title').value || 'Background Noise Study';
|
||||
const projectName = document.getElementById('report-project').value || '';
|
||||
const clientName = document.getElementById('report-client').value || '';
|
||||
const locationName = document.getElementById('report-location').value || '';
|
||||
const startTime = document.getElementById('start-time').value || '';
|
||||
const endTime = document.getElementById('end-time').value || '';
|
||||
const startDate = document.getElementById('start-date').value || '';
|
||||
const endDate = document.getElementById('end-date').value || '';
|
||||
|
||||
// Build the URL with query parameters
|
||||
const params = new URLSearchParams({
|
||||
report_title: reportTitle,
|
||||
location_name: locationName
|
||||
project_name: projectName,
|
||||
client_name: clientName,
|
||||
location_name: locationName,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
});
|
||||
|
||||
// Trigger download
|
||||
window.location.href = `/api/projects/{{ project_id }}/files/{{ file_id }}/generate-report?${params.toString()}`;
|
||||
if (preview) {
|
||||
// Open preview page
|
||||
window.location.href = `/api/projects/{{ project_id }}/files/{{ file_id }}/preview-report?${params.toString()}`;
|
||||
} else {
|
||||
// Direct download
|
||||
window.location.href = `/api/projects/{{ project_id }}/files/{{ file_id }}/generate-report?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Close modal
|
||||
closeReportModal();
|
||||
}
|
||||
|
||||
async function saveAsTemplate() {
|
||||
const name = prompt('Enter a name for this template:');
|
||||
if (!name) return;
|
||||
|
||||
const templateData = {
|
||||
name: name,
|
||||
project_id: '{{ project_id }}',
|
||||
report_title: document.getElementById('report-title').value || 'Background Noise Study',
|
||||
start_time: document.getElementById('start-time').value || null,
|
||||
end_time: document.getElementById('end-time').value || null,
|
||||
start_date: document.getElementById('start-date').value || null,
|
||||
end_date: document.getElementById('end-date').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/report-templates', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(templateData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Template saved successfully!');
|
||||
loadTemplates();
|
||||
} else {
|
||||
alert('Failed to save template');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error saving template: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeReportModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Update preset buttons when time inputs change
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const startTimeInput = document.getElementById('start-time');
|
||||
const endTimeInput = document.getElementById('end-time');
|
||||
if (startTimeInput) startTimeInput.addEventListener('change', updatePresetButtons);
|
||||
if (endTimeInput) endTimeInput.addEventListener('change', updatePresetButtons);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Report Generation Modal -->
|
||||
@@ -499,7 +684,7 @@ document.addEventListener('keydown', function(e) {
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75 transition-opacity" onclick="closeReportModal()"></div>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div class="inline-block align-bottom bg-white dark:bg-slate-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div class="inline-block align-bottom bg-white dark:bg-slate-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
|
||||
<div class="bg-white dark:bg-slate-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-emerald-100 dark:bg-emerald-900/30 sm:mx-0 sm:h-10 sm:w-10">
|
||||
@@ -511,15 +696,53 @@ document.addEventListener('keydown', function(e) {
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
|
||||
Generate Excel Report
|
||||
</h3>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<!-- Template Selection -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1">
|
||||
<label for="template-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Load Template
|
||||
</label>
|
||||
<select id="template-select" onchange="applyTemplate()"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
<option value="">-- Select a template --</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" onclick="saveAsTemplate()"
|
||||
class="mt-6 px-3 py-2 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
title="Save current settings as template">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Report Title -->
|
||||
<div>
|
||||
<label for="report-title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Report Title
|
||||
</label>
|
||||
<input type="text" id="report-title" value="Background Noise Study"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
placeholder="e.g., Background Noise Study - Commercial Street Bridge">
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Project and Client in a row -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="report-project" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Project Name
|
||||
</label>
|
||||
<input type="text" id="report-project" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="report-client" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Client Name
|
||||
</label>
|
||||
<input type="text" id="report-client" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Name -->
|
||||
@@ -528,8 +751,67 @@ document.addEventListener('keydown', function(e) {
|
||||
Location Name
|
||||
</label>
|
||||
<input type="text" id="report-location" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
placeholder="e.g., NRL 1 - West Side">
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Time Filter Section -->
|
||||
<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">
|
||||
Time Filter
|
||||
</label>
|
||||
|
||||
<!-- Preset Buttons -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button type="button" onclick="setTimePreset('night')" data-preset="night"
|
||||
class="preset-btn px-3 py-1.5 text-sm rounded-md bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
7PM - 7AM
|
||||
</button>
|
||||
<button type="button" onclick="setTimePreset('day')" data-preset="day"
|
||||
class="preset-btn px-3 py-1.5 text-sm rounded-md bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
7AM - 7PM
|
||||
</button>
|
||||
<button type="button" onclick="setTimePreset('all')" data-preset="all"
|
||||
class="preset-btn px-3 py-1.5 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-700 transition-colors">
|
||||
All Day
|
||||
</button>
|
||||
<button type="button" onclick="setTimePreset('custom')" data-preset="custom"
|
||||
class="preset-btn px-3 py-1.5 text-sm rounded-md bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom Time Inputs -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="start-time" class="block text-xs text-gray-500 dark:text-gray-400">Start Time</label>
|
||||
<input type="time" id="start-time" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="end-time" class="block text-xs text-gray-500 dark:text-gray-400">End Time</label>
|
||||
<input type="time" id="end-time" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range (Optional) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Date Range <span class="text-gray-400 font-normal">(optional)</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="start-date" class="block text-xs text-gray-500 dark:text-gray-400">From</label>
|
||||
<input type="date" id="start-date" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="end-date" class="block text-xs text-gray-500 dark:text-gray-400">To</label>
|
||||
<input type="date" id="end-date" value=""
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info about what's included -->
|
||||
@@ -547,16 +829,24 @@ document.addEventListener('keydown', function(e) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-2">
|
||||
<button type="button" onclick="generateReport()"
|
||||
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 sm:px-6 flex flex-col sm:flex-row-reverse gap-2">
|
||||
<button type="button" onclick="generateReport(false)"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-emerald-600 text-base font-medium text-white hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:w-auto sm:text-sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Generate & Download
|
||||
Download Excel
|
||||
</button>
|
||||
<button type="button" onclick="generateReport(true)"
|
||||
class="w-full inline-flex justify-center rounded-md border border-emerald-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:w-auto sm:text-sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
Preview & Edit
|
||||
</button>
|
||||
<button type="button" onclick="closeReportModal()"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:mt-0 sm:w-auto sm:text-sm">
|
||||
class="w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
{% if not from_project and not from_nrl %}
|
||||
<!-- Configure button only shown in administrative context (accessed from roster/SLM dashboard) -->
|
||||
<div class="flex gap-3">
|
||||
<button onclick="openConfigModal()"
|
||||
<button onclick="openSLMSettingsModal('{{ unit_id }}')"
|
||||
class="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
@@ -104,73 +104,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Modal -->
|
||||
<div id="config-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 m-4">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Configure {{ unit_id }}</h2>
|
||||
<button onclick="closeConfigModal()" 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>
|
||||
|
||||
<div id="config-modal-content"
|
||||
hx-get="/api/slm-dashboard/config/{{ unit_id }}"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<!-- Loading skeleton -->
|
||||
<div class="p-6 space-y-4 animate-pulse">
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Modal functions
|
||||
function openConfigModal() {
|
||||
const modal = document.getElementById('config-modal');
|
||||
modal.classList.remove('hidden');
|
||||
// Reload config when opening
|
||||
htmx.ajax('GET', '/api/slm-dashboard/config/{{ unit_id }}', {
|
||||
target: '#config-modal-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
function closeConfigModal() {
|
||||
document.getElementById('config-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Keyboard shortcut
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
document.getElementById('config-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for config updates to refresh live view
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.pathInfo.requestPath.includes('/config/') && event.detail.successful) {
|
||||
// Refresh live view after config update
|
||||
htmx.ajax('GET', '/api/slm-dashboard/live-view/{{ unit_id }}', {
|
||||
target: '#live-view-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- Unified SLM Settings Modal -->
|
||||
{% include 'partials/slm_settings_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -137,27 +137,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Modal -->
|
||||
<div id="slm-config-modal" 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 p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">Configure SLM</h3>
|
||||
<button onclick="closeDeviceConfigModal()" 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>
|
||||
|
||||
<div id="slm-config-modal-content">
|
||||
<div class="animate-pulse space-y-4">
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Unified SLM Settings Modal -->
|
||||
{% include 'partials/slm_settings_modal.html' %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
@@ -365,33 +346,23 @@ function updateDashboardChart(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration modal
|
||||
// Configuration modal - use unified SLM settings modal
|
||||
function openDeviceConfigModal(unitId) {
|
||||
const modal = document.getElementById('slm-config-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
htmx.ajax('GET', `/api/slm-dashboard/config/${unitId}`, {
|
||||
target: '#slm-config-modal-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
// Call the unified modal function from slm_settings_modal.html
|
||||
if (typeof openSLMSettingsModal === 'function') {
|
||||
openSLMSettingsModal(unitId);
|
||||
} else {
|
||||
console.error('openSLMSettingsModal not found');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeviceConfigModal() {
|
||||
document.getElementById('slm-config-modal').classList.add('hidden');
|
||||
// Call the unified modal close function
|
||||
if (typeof closeSLMSettingsModal === 'function') {
|
||||
closeSLMSettingsModal();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeDeviceConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('slm-config-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeviceConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
stopDashboardStream();
|
||||
|
||||
Reference in New Issue
Block a user