Files
terra-view/templates/combined_report_wizard.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 overflow-hidden">
<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 %}