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:
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 %}
|
||||
Reference in New Issue
Block a user