diff --git a/app/controllers/measurements_controller.rb b/app/controllers/measurements_controller.rb index b824cf1..87b1975 100644 --- a/app/controllers/measurements_controller.rb +++ b/app/controllers/measurements_controller.rb @@ -6,7 +6,7 @@ class MeasurementsController < ApplicationController end def index - @measurements = current_user.readouts.includes(:quantity, :unit).order(taken_at: :desc, id: :desc) + load_measurements end def new @@ -20,6 +20,7 @@ class MeasurementsController < ApplicationController if @readouts.present? && @readouts.all?(&:valid?) ActiveRecord::Base.transaction { @readouts.each(&:save!) } + load_measurements flash.now[:notice] = t('.success', count: @readouts.size) else errors = @readouts.flat_map { |r| r.errors.full_messages } @@ -33,6 +34,7 @@ class MeasurementsController < ApplicationController def update if @readout.update(params.require(:readout).permit(:value, :unit_id, :taken_at)) + load_measurements flash.now[:notice] = t('.success') else @user_units = current_user.units.ordered @@ -42,6 +44,7 @@ class MeasurementsController < ApplicationController def destroy @readout.destroy! + load_measurements flash.now[:notice] = t('.success') end @@ -50,4 +53,10 @@ class MeasurementsController < ApplicationController def find_readout @readout = current_user.readouts.find(params[:id]) 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 diff --git a/app/javascript/application.js b/app/javascript/application.js index d860e77..81be3a4 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -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() { var container = document.getElementById('measurements-charts'); @@ -169,7 +77,6 @@ function getMeasurementsView() { function applyMeasurementsView(view) { document.body.dataset.measurementsView = view; - if (view === 'wide') buildWideTable(); } function setMeasurementsView(view) { @@ -189,8 +96,6 @@ document.addEventListener('turbo:load', function() { applyMeasurementsView(getMeasurementsView()); new MutationObserver(function() { groupMeasurements(); - var view = getMeasurementsView(); - if (view === 'wide') buildWideTable(); }).observe(tbody, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] @@ -225,26 +130,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'); diff --git a/app/views/measurements/_wide_table.html.erb b/app/views/measurements/_wide_table.html.erb new file mode 100644 index 0000000..e9be2be --- /dev/null +++ b/app/views/measurements/_wide_table.html.erb @@ -0,0 +1,41 @@ + + + + + <% wide_quantities.each do |q| %> + + <% end %> + + + + + <% wide_groups.each do |taken_at, readouts| %> + + + <% wide_quantities.each do |q| %> + <% readout = readouts.find { |r| r.quantity_id == q.id } %> + + <% end %> + + + <% end %> + +
<%= Readout.human_attribute_name(:taken_at) %><%= q.name %><%= Readout.human_attribute_name(:created_at) %>
<%= l(taken_at) if taken_at %> + <% if readout %> + + <% 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 %> + + <% end %> + <%= l(readouts.first.created_at) %>
diff --git a/app/views/measurements/create.turbo_stream.erb b/app/views/measurements/create.turbo_stream.erb index 85e39ed..3137c0d 100644 --- a/app/views/measurements/create.turbo_stream.erb +++ b/app/views/measurements/create.turbo_stream.erb @@ -1,4 +1,4 @@ -<% if @readouts.all?(&:persisted?) %> +<% if @readouts.present? && @readouts.all?(&:persisted?) %> <%= turbo_stream.update :flashes %> <%= turbo_stream.remove :measurement_form %> <%= turbo_stream.enable :new_measurement_link %> @@ -6,6 +6,8 @@ <% @readouts.each do |readout| %> <%= turbo_stream.prepend :measurements, partial: 'readout', locals: {readout: readout} %> <% end %> + <%= turbo_stream.update 'measurements-wide', partial: 'wide_table', + locals: {wide_groups: @wide_groups, wide_quantities: @wide_quantities} %> <% else %> <%= turbo_stream.update :flashes %> <% end %> diff --git a/app/views/measurements/destroy.turbo_stream.erb b/app/views/measurements/destroy.turbo_stream.erb index ac0c032..0487bfd 100644 --- a/app/views/measurements/destroy.turbo_stream.erb +++ b/app/views/measurements/destroy.turbo_stream.erb @@ -1,3 +1,5 @@ <%= turbo_stream.update :flashes %> <%= turbo_stream.remove @readout %> <%= 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} %> diff --git a/app/views/measurements/index.html.erb b/app/views/measurements/index.html.erb index 372ba72..8d5c5b2 100644 --- a/app/views/measurements/index.html.erb +++ b/app/views/measurements/index.html.erb @@ -34,7 +34,8 @@ -
- +
+ <%= render 'wide_table', wide_groups: @wide_groups, wide_quantities: @wide_quantities %> +
diff --git a/app/views/measurements/update.turbo_stream.erb b/app/views/measurements/update.turbo_stream.erb index 36f4d44..dd9b9c4 100644 --- a/app/views/measurements/update.turbo_stream.erb +++ b/app/views/measurements/update.turbo_stream.erb @@ -1,2 +1,4 @@ <%= turbo_stream.close_form dom_id(@readout, :edit) %> <%= 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} %>