feat(reports): add "Night Report" button to sound project header

Sound projects only: a Night Report button next to "Generate Combined Report"
opens a small modal (pick night + optional baseline range) that opens the
rendered report (/reports/nightly/view) in a new tab. Defaults the night to
last night; baseline is optional.

Verified the header partial renders and the button is gated to sound_monitoring
(hidden on vibration-only projects); modal + JS wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 00:59:50 +00:00
parent 786a9821a3
commit 08d3d53702
@@ -74,6 +74,14 @@
</svg> </svg>
Generate Combined Report Generate Combined Report
</a> </a>
<button onclick="openNightReportModal()"
title="Last night's noise vs baseline, per location (FTP report pipeline)"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center gap-2 text-sm">
<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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
Night Report
</button>
{% endif %} {% endif %}
<button onclick="openMergeModal()" <button onclick="openMergeModal()"
title="Merge this project into another (consolidates duplicates)" title="Merge this project into another (consolidates duplicates)"
@@ -87,6 +95,65 @@
</div> </div>
</div> </div>
<!-- Night Report Modal -->
<div id="night-report-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-md mx-4">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Night Report</h3>
<button onclick="closeNightReportModal()" class="text-gray-400 hover:text-gray-600 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 class="px-6 py-5 space-y-4">
<p class="text-xs text-gray-500 dark:text-gray-400">Last night's noise (7&nbsp;PM7&nbsp;AM) vs a baseline range, per location. Opens in a new tab.</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Night (evening date)</label>
<input type="date" id="nr-night-date" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baseline start <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="date" id="nr-baseline-start" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-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">Baseline end</label>
<input type="date" id="nr-baseline-end" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
</div>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
<button onclick="closeNightReportModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm">Cancel</button>
<button onclick="viewNightReport('{{ project.id }}')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors text-sm">View Report</button>
</div>
</div>
</div>
<script>
function openNightReportModal() {
var el = document.getElementById('nr-night-date');
if (el && !el.value) { // default to last night
var d = new Date(); d.setDate(d.getDate() - 1);
el.value = d.getFullYear() + '-'
+ String(d.getMonth() + 1).padStart(2, '0') + '-'
+ String(d.getDate()).padStart(2, '0');
}
document.getElementById('night-report-modal').classList.remove('hidden');
}
function closeNightReportModal() {
document.getElementById('night-report-modal').classList.add('hidden');
}
function viewNightReport(projectId) {
var night = document.getElementById('nr-night-date').value;
var bs = document.getElementById('nr-baseline-start').value;
var be = document.getElementById('nr-baseline-end').value;
if (!night) { alert('Pick a night (evening date).'); return; }
if ((bs && !be) || (be && !bs)) { alert('Provide both baseline dates, or leave both empty.'); return; }
var url = '/api/projects/' + projectId + '/reports/nightly/view?night_date=' + night;
if (bs && be) url += '&baseline_start=' + bs + '&baseline_end=' + be;
window.open(url, '_blank');
closeNightReportModal();
}
</script>
<!-- Merge Modal — <!-- Merge Modal —
min-h on the body ensures the typeahead dropdown has room to render min-h on the body ensures the typeahead dropdown has room to render
below the input without forcing the operator to scroll inside the below the input without forcing the operator to scroll inside the