Replace JS-generated wide table with ERB partial and Turbo Streams

- Add _wide_table.html.erb partial (server-rendered pivot table)
- Add load_measurements helper in controller to prepare @wide_groups and
  @wide_quantities for all mutating actions
- Update index view to render the wide_table partial in #measurements-wide
- Add/update create, destroy, update turbo_stream views to refresh the
  wide table atomically after each mutation
- Remove buildWideTable() and editMeasurementWide() from application.js
- Fix create.turbo_stream.erb condition (empty readouts are vacuously all persisted)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:28:32 +00:00
parent 1bc75f5d40
commit 366662a948
7 changed files with 74 additions and 116 deletions

View File

@@ -22,98 +22,6 @@ function groupMeasurements() {
});
}
function buildWideTable() {
var tbody = document.getElementById('measurements');
var wideContainer = document.getElementById('measurements-wide');
if (!tbody || !wideContainer) return;
var rows = Array.from(tbody.querySelectorAll('tr[data-taken-at]'));
if (rows.length === 0) { wideContainer.innerHTML = ''; return; }
// Unique quantities in alphabetical order
var qOrder = [], qSeen = new Set();
rows.forEach(function(r) {
var id = r.dataset.quantityId;
if (id && !qSeen.has(id)) { qSeen.add(id); qOrder.push({id: id, name: r.dataset.quantityName}); }
});
qOrder.sort(function(a, b) { return a.name.localeCompare(b.name); });
// Group rows by taken_at, preserving order
var groups = [], groupMap = new Map();
rows.forEach(function(r) {
var key = r.dataset.takenAt || '';
if (!groupMap.has(key)) { var g = {rows: []}; groups.push(g); groupMap.set(key, g); }
groupMap.get(key).rows.push(r);
});
var takenAtHeader = (document.querySelector('[data-column="taken-at"]') || {}).textContent || '';
var createdAtHeader = (document.querySelector('[data-column="created-at"]') || {}).textContent || '';
var table = document.createElement('table');
table.className = 'items-table';
// Header
var thead = table.createTHead();
var hrow = thead.insertRow();
[takenAtHeader].concat(qOrder.map(function(q) { return q.name; })).concat([createdAtHeader]).forEach(function(text) {
var th = document.createElement('th');
th.textContent = text;
hrow.appendChild(th);
});
// Body
var tbodyEl = table.createTBody();
groups.forEach(function(group) {
var tr = tbodyEl.insertRow();
// Taken at
var tdTime = tr.insertCell();
var takenAtEl = group.rows[0].querySelector('.taken-at');
tdTime.textContent = takenAtEl ? takenAtEl.textContent : '';
// One cell per quantity
qOrder.forEach(function(q) {
var td = tr.insertCell();
var readoutRow = group.rows.find(function(r) { return r.dataset.quantityId === q.id; });
if (readoutRow) {
td.className = 'ralign';
var wrap = document.createElement('span');
wrap.className = 'wide-cell';
var editLink = readoutRow.querySelector('a.link');
if (editLink) {
var editUrl = editLink.href + (editLink.href.includes('?') ? '&' : '?') + 'view=wide';
var btn = document.createElement('button');
btn.className = 'link';
btn.type = 'button';
btn.dataset.editUrl = editUrl;
btn.addEventListener('click', function() { editMeasurementWide(this.dataset.editUrl); this.blur(); });
btn.textContent = readoutRow.dataset.value;
wrap.appendChild(btn);
wrap.appendChild(document.createTextNode('\u00a0' + readoutRow.dataset.unit));
} else {
wrap.appendChild(document.createTextNode(readoutRow.dataset.value + '\u00a0' + readoutRow.dataset.unit));
}
var srcActions = readoutRow.querySelector('td.flex');
if (srcActions) srcActions.querySelectorAll('form').forEach(function(f) {
var cloned = f.cloneNode(true);
var span = cloned.querySelector('button span');
if (span) span.remove();
wrap.appendChild(cloned);
});
td.appendChild(wrap);
}
});
// Created at (from first row of group)
var tdCreated = tr.insertCell();
var createdAtEl = group.rows[0].querySelector('.created-at');
tdCreated.textContent = createdAtEl ? createdAtEl.textContent : '';
});
wideContainer.innerHTML = '';
wideContainer.appendChild(table);
}
function getMeasurementsView() {
return localStorage.getItem('measurements-view') || 'compact';
@@ -121,7 +29,6 @@ function getMeasurementsView() {
function applyMeasurementsView(view) {
document.body.dataset.measurementsView = view;
if (view === 'wide') buildWideTable();
}
function setMeasurementsView(view) {
@@ -137,7 +44,6 @@ document.addEventListener('turbo:load', function() {
applyMeasurementsView(getMeasurementsView());
new MutationObserver(function() {
groupMeasurements();
if (getMeasurementsView() === 'wide') buildWideTable();
}).observe(tbody, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['style']
@@ -172,26 +78,6 @@ window.detailsObserver = new MutationObserver((mutations) => {
mutations[0].target.dispatchEvent(new Event('change', {bubbles: true}))
});
function editMeasurementWide(url) {
fetch(url, {
headers: {
'Accept': 'text/vnd.turbo-stream.html',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
Turbo.renderStreamMessage(html);
requestAnimationFrame(() => {
var panel = document.getElementById('measurement_edit_form');
if (panel && panel.firstElementChild) {
panel.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
});
})
.catch(err => console.error('editMeasurementWide failed:', err));
}
window.editMeasurementWide = editMeasurementWide
function readoutUnitChanged(select) {
var button = select.closest('tr').querySelector('.set-default-unit');