forked from fixin.me/fixin.me
Merge feature/measurements-wide-view into demo/example-data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ class MeasurementsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@measurements = current_user.readouts.includes(:quantity, :unit).order(taken_at: :desc, id: :desc)
|
load_measurements
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@@ -20,6 +20,7 @@ class MeasurementsController < ApplicationController
|
|||||||
|
|
||||||
if @readouts.present? && @readouts.all?(&:valid?)
|
if @readouts.present? && @readouts.all?(&:valid?)
|
||||||
ActiveRecord::Base.transaction { @readouts.each(&:save!) }
|
ActiveRecord::Base.transaction { @readouts.each(&:save!) }
|
||||||
|
load_measurements
|
||||||
flash.now[:notice] = t('.success', count: @readouts.size)
|
flash.now[:notice] = t('.success', count: @readouts.size)
|
||||||
else
|
else
|
||||||
errors = @readouts.flat_map { |r| r.errors.full_messages }
|
errors = @readouts.flat_map { |r| r.errors.full_messages }
|
||||||
@@ -33,6 +34,7 @@ class MeasurementsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @readout.update(params.require(:readout).permit(:value, :unit_id, :taken_at))
|
if @readout.update(params.require(:readout).permit(:value, :unit_id, :taken_at))
|
||||||
|
load_measurements
|
||||||
flash.now[:notice] = t('.success')
|
flash.now[:notice] = t('.success')
|
||||||
else
|
else
|
||||||
@user_units = current_user.units.ordered
|
@user_units = current_user.units.ordered
|
||||||
@@ -42,6 +44,7 @@ class MeasurementsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@readout.destroy!
|
@readout.destroy!
|
||||||
|
load_measurements
|
||||||
flash.now[:notice] = t('.success')
|
flash.now[:notice] = t('.success')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,4 +53,10 @@ class MeasurementsController < ApplicationController
|
|||||||
def find_readout
|
def find_readout
|
||||||
@readout = current_user.readouts.find(params[:id])
|
@readout = current_user.readouts.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_measurements
|
||||||
|
@measurements = current_user.readouts.includes(:quantity, :unit).order(taken_at: :desc, id: :desc)
|
||||||
|
@wide_groups = @measurements.group_by(&:taken_at)
|
||||||
|
@wide_quantities = @measurements.map(&:quantity).uniq.sort_by(&:name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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 buildCharts() {
|
function buildCharts() {
|
||||||
var container = document.getElementById('measurements-charts');
|
var container = document.getElementById('measurements-charts');
|
||||||
@@ -169,7 +77,6 @@ function getMeasurementsView() {
|
|||||||
|
|
||||||
function applyMeasurementsView(view) {
|
function applyMeasurementsView(view) {
|
||||||
document.body.dataset.measurementsView = view;
|
document.body.dataset.measurementsView = view;
|
||||||
if (view === 'wide') buildWideTable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMeasurementsView(view) {
|
function setMeasurementsView(view) {
|
||||||
@@ -189,8 +96,6 @@ document.addEventListener('turbo:load', function() {
|
|||||||
applyMeasurementsView(getMeasurementsView());
|
applyMeasurementsView(getMeasurementsView());
|
||||||
new MutationObserver(function() {
|
new MutationObserver(function() {
|
||||||
groupMeasurements();
|
groupMeasurements();
|
||||||
var view = getMeasurementsView();
|
|
||||||
if (view === 'wide') buildWideTable();
|
|
||||||
}).observe(tbody, {
|
}).observe(tbody, {
|
||||||
childList: true, subtree: true,
|
childList: true, subtree: true,
|
||||||
attributes: true, attributeFilter: ['style']
|
attributes: true, attributeFilter: ['style']
|
||||||
@@ -225,26 +130,6 @@ window.detailsObserver = new MutationObserver((mutations) => {
|
|||||||
mutations[0].target.dispatchEvent(new Event('change', {bubbles: true}))
|
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) {
|
function readoutUnitChanged(select) {
|
||||||
var button = select.closest('tr').querySelector('.set-default-unit');
|
var button = select.closest('tr').querySelector('.set-default-unit');
|
||||||
|
|||||||
41
app/views/measurements/_wide_table.html.erb
Normal file
41
app/views/measurements/_wide_table.html.erb
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%= Readout.human_attribute_name(:taken_at) %></th>
|
||||||
|
<% wide_quantities.each do |q| %>
|
||||||
|
<th><%= q.name %></th>
|
||||||
|
<% end %>
|
||||||
|
<th><%= Readout.human_attribute_name(:created_at) %></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% wide_groups.each do |taken_at, readouts| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= l(taken_at) if taken_at %></td>
|
||||||
|
<% wide_quantities.each do |q| %>
|
||||||
|
<% readout = readouts.find { |r| r.quantity_id == q.id } %>
|
||||||
|
<td class="ralign">
|
||||||
|
<% if readout %>
|
||||||
|
<span class="wide-cell">
|
||||||
|
<% if current_user.at_least(:active) %>
|
||||||
|
<%= link_to format("%.10g", readout.value),
|
||||||
|
edit_measurement_path(readout, view: :wide),
|
||||||
|
class: 'link', onclick: 'this.blur();',
|
||||||
|
data: {turbo_stream: true} %>
|
||||||
|
<% else %>
|
||||||
|
<%= format("%.10g", readout.value) %>
|
||||||
|
<% end %>
|
||||||
|
<%= readout.unit.symbol %>
|
||||||
|
<% if current_user.at_least(:active) %>
|
||||||
|
<%= image_button_to '', 'delete-outline', measurement_path(readout),
|
||||||
|
method: :delete, data: {turbo_stream: true} %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
<td><%= l(readouts.first.created_at) %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<% if @readouts.all?(&:persisted?) %>
|
<% if @readouts.present? && @readouts.all?(&:persisted?) %>
|
||||||
<%= turbo_stream.update :flashes %>
|
<%= turbo_stream.update :flashes %>
|
||||||
<%= turbo_stream.remove :measurement_form %>
|
<%= turbo_stream.remove :measurement_form %>
|
||||||
<%= turbo_stream.enable :new_measurement_link %>
|
<%= turbo_stream.enable :new_measurement_link %>
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
<% @readouts.each do |readout| %>
|
<% @readouts.each do |readout| %>
|
||||||
<%= turbo_stream.prepend :measurements, partial: 'readout', locals: {readout: readout} %>
|
<%= turbo_stream.prepend :measurements, partial: 'readout', locals: {readout: readout} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= turbo_stream.update 'measurements-wide', partial: 'wide_table',
|
||||||
|
locals: {wide_groups: @wide_groups, wide_quantities: @wide_quantities} %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= turbo_stream.update :flashes %>
|
<%= turbo_stream.update :flashes %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
<%= turbo_stream.update :flashes %>
|
<%= turbo_stream.update :flashes %>
|
||||||
<%= turbo_stream.remove @readout %>
|
<%= turbo_stream.remove @readout %>
|
||||||
<%= turbo_stream.append(:measurements, render_no_items) if current_user.readouts.empty? %>
|
<%= turbo_stream.append(:measurements, render_no_items) if current_user.readouts.empty? %>
|
||||||
|
<%= turbo_stream.update 'measurements-wide', partial: 'wide_table',
|
||||||
|
locals: {wide_groups: @wide_groups, wide_quantities: @wide_quantities} %>
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id="measurements-wide" class="measurements-wide"></div>
|
<div id="measurements-wide" class="measurements-wide">
|
||||||
|
<%= render 'wide_table', wide_groups: @wide_groups, wide_quantities: @wide_quantities %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
<%= turbo_stream.close_form dom_id(@readout, :edit) %>
|
<%= turbo_stream.close_form dom_id(@readout, :edit) %>
|
||||||
<%= turbo_stream.replace @readout, partial: 'measurements/readout', locals: {readout: @readout} %>
|
<%= turbo_stream.replace @readout, partial: 'measurements/readout', locals: {readout: @readout} %>
|
||||||
|
<%= turbo_stream.update 'measurements-wide', partial: 'wide_table',
|
||||||
|
locals: {wide_groups: @wide_groups, wide_quantities: @wide_quantities} %>
|
||||||
|
|||||||
Reference in New Issue
Block a user