394 lines
22 KiB
HTML
394 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Combined Report Wizard - {{ project.name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<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-4xl 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">Combined Report Wizard</h1>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ project.name }}</p>
|
|
</div>
|
|
<a href="/projects/{{ project_id }}"
|
|
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 text-sm w-fit">
|
|
← Back to Project
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
|
|
|
<!-- Report Settings Card -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Report Settings</h2>
|
|
|
|
<!-- Template Selection -->
|
|
<div class="flex items-end gap-2 mb-4">
|
|
<div class="flex-1">
|
|
<label for="template-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Load Template
|
|
</label>
|
|
<select id="template-select" onchange="applyTemplate()"
|
|
class="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="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 class="mb-4">
|
|
<label for="report-title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Report Title
|
|
</label>
|
|
<input type="text" id="report-title" value="Background Noise Study"
|
|
class="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 -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="report-project" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Project Name
|
|
</label>
|
|
<input type="text" id="report-project" value="{{ project.name }}"
|
|
class="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 mb-1">
|
|
Client Name
|
|
</label>
|
|
<input type="text" id="report-client" value="{{ project.client_name if project.client_name else '' }}"
|
|
class="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>
|
|
|
|
<!-- Sessions Card -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Monitoring Sessions</h2>
|
|
<div class="flex gap-3 text-sm">
|
|
<button type="button" onclick="selectAllSessions()" class="text-emerald-600 dark:text-emerald-400 hover:underline">Select All</button>
|
|
<button type="button" onclick="deselectAllSessions()" class="text-gray-500 dark:text-gray-400 hover:underline">Deselect All</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
<span id="selected-count">0</span> session(s) selected — each selected session becomes one sheet in the ZIP.
|
|
Change the period type per session to control how stats are bucketed (Day vs Night).
|
|
</p>
|
|
|
|
{% if locations %}
|
|
{% for loc in locations %}
|
|
{% set loc_name = loc.name %}
|
|
{% set sessions = loc.sessions %}
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg mb-3 overflow-hidden">
|
|
<!-- Location header / toggle -->
|
|
<button type="button"
|
|
onclick="toggleLocation('loc-{{ loop.index }}')"
|
|
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-slate-700/50 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors text-left">
|
|
<div class="flex items-center gap-3">
|
|
<svg id="chevron-loc-{{ loop.index }}" class="w-4 h-4 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
<span class="font-medium text-gray-900 dark:text-white text-sm">{{ loc_name }}</span>
|
|
<span class="text-xs text-gray-400 dark:text-gray-500">{{ sessions|length }} session{{ 's' if sessions|length != 1 else '' }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 text-xs" onclick="event.stopPropagation()">
|
|
<button type="button" onclick="selectLocation('loc-{{ loop.index }}')"
|
|
class="text-emerald-600 dark:text-emerald-400 hover:underline">All</button>
|
|
<button type="button" onclick="deselectLocation('loc-{{ loop.index }}')"
|
|
class="text-gray-400 hover:underline">None</button>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- Session rows -->
|
|
<div id="loc-{{ loop.index }}" class="divide-y divide-gray-100 dark:divide-gray-700/50">
|
|
{% for s in sessions %}
|
|
{% set pt_colors = {
|
|
'weekday_day': 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
|
'weekday_night': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300',
|
|
'weekend_day': 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
|
'weekend_night': 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
|
} %}
|
|
{% set pt_labels = {
|
|
'weekday_day': 'Weekday Day',
|
|
'weekday_night': 'Weekday Night',
|
|
'weekend_day': 'Weekend Day',
|
|
'weekend_night': 'Weekend Night',
|
|
} %}
|
|
<div class="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-slate-700/30 transition-colors">
|
|
<!-- Checkbox -->
|
|
<input type="checkbox"
|
|
class="session-cb loc-{{ loop.index }}-cb h-4 w-4 text-emerald-600 border-gray-300 dark:border-gray-600 rounded focus:ring-emerald-500 shrink-0"
|
|
value="{{ s.session_id }}"
|
|
checked
|
|
onchange="updateSelectionStats()">
|
|
|
|
<!-- Date/day info -->
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ s.day_of_week }} {{ s.date_display }}
|
|
</span>
|
|
{% if s.session_label %}
|
|
<span class="text-xs text-gray-400 dark:text-gray-500 truncate">{{ s.session_label }}</span>
|
|
{% endif %}
|
|
{% if s.status == 'recording' %}
|
|
<span class="px-1.5 py-0.5 text-xs bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 rounded-full flex items-center gap-1">
|
|
<span class="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse"></span>Recording
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex items-center gap-3 mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
|
{% if s.started_at %}
|
|
<span>{{ s.started_at }}</span>
|
|
{% endif %}
|
|
{% if s.duration_h is not none %}
|
|
<span>{{ s.duration_h }}h {{ s.duration_m }}m</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Period type dropdown -->
|
|
<div class="relative shrink-0" id="wiz-period-wrap-{{ s.session_id }}">
|
|
<button type="button"
|
|
onclick="toggleWizPeriodMenu('{{ s.session_id }}')"
|
|
id="wiz-period-badge-{{ s.session_id }}"
|
|
class="px-2 py-0.5 text-xs font-medium rounded-full flex items-center gap-1 transition-colors {{ pt_colors.get(s.period_type, 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400') }}"
|
|
title="Click to change period type">
|
|
<span id="wiz-period-label-{{ s.session_id }}">{{ pt_labels.get(s.period_type, 'Set period') }}</span>
|
|
<svg class="w-3 h-3 opacity-60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</button>
|
|
<div id="wiz-period-menu-{{ s.session_id }}"
|
|
class="hidden absolute right-0 top-full mt-1 z-20 bg-white dark:bg-slate-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg min-w-[160px] py-1">
|
|
{% for pt, pt_label in [('weekday_day','Weekday Day'),('weekday_night','Weekday Night'),('weekend_day','Weekend Day'),('weekend_night','Weekend Night')] %}
|
|
<button type="button"
|
|
onclick="setWizPeriodType('{{ s.session_id }}', '{{ pt }}')"
|
|
class="w-full text-left px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-slate-600 text-gray-700 dark:text-gray-300 {% if s.period_type == pt %}font-bold{% endif %}">
|
|
{{ pt_label }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-10 text-gray-500 dark:text-gray-400">
|
|
<svg class="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" 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>
|
|
<p>No monitoring sessions found.</p>
|
|
<p class="text-sm mt-1">Upload data files to create sessions first.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Footer Buttons -->
|
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 pb-6">
|
|
<a href="/projects/{{ project_id }}"
|
|
class="w-full sm:w-auto px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors text-center text-sm font-medium">
|
|
Cancel
|
|
</a>
|
|
<button type="button" onclick="gotoPreview()" id="preview-btn"
|
|
{% if not locations %}disabled{% endif %}
|
|
class="w-full sm:w-auto px-6 py-2.5 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<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="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>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const PROJECT_ID = '{{ project_id }}';
|
|
|
|
const PERIOD_COLORS = {
|
|
weekday_day: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
|
weekday_night: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300',
|
|
weekend_day: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
|
weekend_night: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
|
};
|
|
const PERIOD_LABELS = {
|
|
weekday_day: 'Weekday Day',
|
|
weekday_night: 'Weekday Night',
|
|
weekend_day: 'Weekend Day',
|
|
weekend_night: 'Weekend Night',
|
|
};
|
|
const ALL_PERIOD_BADGE_CLASSES = [
|
|
'bg-gray-100','text-gray-500','dark:bg-gray-700','dark:text-gray-400',
|
|
...new Set(Object.values(PERIOD_COLORS).flatMap(s => s.split(' ')))
|
|
];
|
|
|
|
// ── Location accordion ────────────────────────────────────────────
|
|
|
|
function toggleLocation(locId) {
|
|
const body = document.getElementById(locId);
|
|
const chevron = document.getElementById('chevron-' + locId);
|
|
body.classList.toggle('hidden');
|
|
chevron.style.transform = body.classList.contains('hidden') ? 'rotate(-90deg)' : '';
|
|
}
|
|
|
|
function selectLocation(locId) {
|
|
document.querySelectorAll('.' + locId + '-cb').forEach(cb => cb.checked = true);
|
|
updateSelectionStats();
|
|
}
|
|
|
|
function deselectLocation(locId) {
|
|
document.querySelectorAll('.' + locId + '-cb').forEach(cb => cb.checked = false);
|
|
updateSelectionStats();
|
|
}
|
|
|
|
function selectAllSessions() {
|
|
document.querySelectorAll('.session-cb').forEach(cb => cb.checked = true);
|
|
updateSelectionStats();
|
|
}
|
|
|
|
function deselectAllSessions() {
|
|
document.querySelectorAll('.session-cb').forEach(cb => cb.checked = false);
|
|
updateSelectionStats();
|
|
}
|
|
|
|
function updateSelectionStats() {
|
|
const count = document.querySelectorAll('.session-cb:checked').length;
|
|
document.getElementById('selected-count').textContent = count;
|
|
document.getElementById('preview-btn').disabled = count === 0;
|
|
}
|
|
|
|
// ── Period type dropdown (wizard) ─────────────────────────────────
|
|
|
|
function toggleWizPeriodMenu(sessionId) {
|
|
const menu = document.getElementById('wiz-period-menu-' + sessionId);
|
|
document.querySelectorAll('[id^="wiz-period-menu-"]').forEach(m => {
|
|
if (m.id !== 'wiz-period-menu-' + sessionId) m.classList.add('hidden');
|
|
});
|
|
menu.classList.toggle('hidden');
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('[id^="wiz-period-wrap-"]')) {
|
|
document.querySelectorAll('[id^="wiz-period-menu-"]').forEach(m => m.classList.add('hidden'));
|
|
}
|
|
});
|
|
|
|
async function setWizPeriodType(sessionId, periodType) {
|
|
document.getElementById('wiz-period-menu-' + sessionId).classList.add('hidden');
|
|
const badge = document.getElementById('wiz-period-badge-' + sessionId);
|
|
badge.disabled = true;
|
|
try {
|
|
const resp = await fetch(`/api/projects/${PROJECT_ID}/sessions/${sessionId}`, {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({period_type: periodType}),
|
|
});
|
|
if (!resp.ok) throw new Error(await resp.text());
|
|
ALL_PERIOD_BADGE_CLASSES.forEach(c => badge.classList.remove(c));
|
|
const colorStr = PERIOD_COLORS[periodType] || 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400';
|
|
badge.classList.add(...colorStr.split(' ').filter(Boolean));
|
|
document.getElementById('wiz-period-label-' + sessionId).textContent = PERIOD_LABELS[periodType] || periodType;
|
|
} catch(err) {
|
|
alert('Failed to update period type: ' + err.message);
|
|
} finally {
|
|
badge.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Template management ───────────────────────────────────────────
|
|
|
|
let reportTemplates = [];
|
|
|
|
async function loadTemplates() {
|
|
try {
|
|
const resp = await fetch('/api/report-templates?project_id=' + PROJECT_ID);
|
|
if (resp.ok) {
|
|
reportTemplates = await resp.json();
|
|
populateTemplateDropdown();
|
|
}
|
|
} catch(e) { console.error('Error loading templates:', e); }
|
|
}
|
|
|
|
function populateTemplateDropdown() {
|
|
const select = document.getElementById('template-select');
|
|
if (!select) return;
|
|
select.innerHTML = '<option value="">-- Select a template --</option>';
|
|
reportTemplates.forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = t.id;
|
|
opt.textContent = t.name;
|
|
opt.dataset.config = JSON.stringify(t);
|
|
select.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
function applyTemplate() {
|
|
const select = document.getElementById('template-select');
|
|
const opt = select.options[select.selectedIndex];
|
|
if (!opt.value) return;
|
|
const t = JSON.parse(opt.dataset.config);
|
|
if (t.report_title) document.getElementById('report-title').value = t.report_title;
|
|
}
|
|
|
|
async function saveAsTemplate() {
|
|
const name = prompt('Enter a name for this template:');
|
|
if (!name) return;
|
|
const data = {
|
|
name,
|
|
project_id: PROJECT_ID,
|
|
report_title: document.getElementById('report-title').value || 'Background Noise Study',
|
|
};
|
|
try {
|
|
const resp = await fetch('/api/report-templates', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (resp.ok) { alert('Template saved!'); loadTemplates(); }
|
|
else alert('Failed to save template');
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
}
|
|
|
|
// ── Navigate to preview ───────────────────────────────────────────
|
|
|
|
function gotoPreview() {
|
|
const checked = Array.from(document.querySelectorAll('.session-cb:checked')).map(cb => cb.value);
|
|
if (checked.length === 0) {
|
|
alert('Please select at least one session.');
|
|
return;
|
|
}
|
|
const params = new URLSearchParams({
|
|
report_title: document.getElementById('report-title').value || 'Background Noise Study',
|
|
project_name: document.getElementById('report-project').value || '',
|
|
client_name: document.getElementById('report-client').value || '',
|
|
selected_sessions: checked.join(','),
|
|
});
|
|
window.location.href = `/api/projects/${PROJECT_ID}/combined-report-preview?${params.toString()}`;
|
|
}
|
|
|
|
// ── Init ─────────────────────────────────────────────────────────
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateSelectionStats();
|
|
loadTemplates();
|
|
});
|
|
</script>
|
|
{% endblock %}
|