Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7775c9b
first shot
lodewiges Jan 21, 2026
a884ff7
run yarn install
lodewiges Jan 21, 2026
cb7b4ed
run the migrations
lodewiges Jan 21, 2026
b010cf4
fix edit mode button
lodewiges Jan 21, 2026
b0924b3
Merge branch 'staging' into feature/redesign-orderscreen
lodewiges Feb 1, 2026
d236a84
puntjes op de i
lodewiges Feb 3, 2026
b2974c7
Merge branch 'feature/redesign-orderscreen' of https://github.com/csv…
lodewiges Feb 3, 2026
dcdba74
fix some lint
lodewiges Feb 4, 2026
4b8babb
remove comments
lodewiges Feb 4, 2026
53c7df1
remove active session logic
lodewiges Feb 4, 2026
a5dedaa
re add a couple comments
lodewiges Feb 4, 2026
b7a3961
run yarn install
lodewiges Feb 4, 2026
4369fc5
lint fixes
lodewiges Feb 4, 2026
5329711
Update app/javascript/order_screen.js
lodewiges Feb 4, 2026
51ecf4d
Update app/controllers/product_prices_controller.rb
lodewiges Feb 4, 2026
99955cf
Update app/javascript/order_screen.js
lodewiges Feb 4, 2026
c5bd856
Update app/javascript/order_screen.js
lodewiges Feb 4, 2026
08d6ae5
implement copilot changes
lodewiges Feb 4, 2026
ab71c13
Apply suggestions from code review
lodewiges Feb 4, 2026
4f2c9a5
some more suggestions
lodewiges Feb 4, 2026
20bd86c
some more improvements
lodewiges Feb 4, 2026
adf39c0
Merge branch 'feature/redesign-orderscreen' of https://github.com/csv…
lodewiges Feb 4, 2026
acfa7a6
Update app/models/price_list.rb
lodewiges Feb 4, 2026
a0091fb
Update app/javascript/order_screen.js
lodewiges Feb 4, 2026
778d290
Update app/models/product_price_folder.rb
lodewiges Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 191 additions & 3 deletions app/assets/stylesheets/order_screen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@

.flash {
display: flex;
align-self: flex-end;
height: 100%;
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 1000;
height: auto;
min-width: 250px;
max-width: 450px;
font-size: $font-size-lg;
vertical-align: middle;
color: $font-color-light;
text-align: center;
background-color: $gray-800;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.flash-text {
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
padding: 1rem 1.5rem;

a {
&:hover {
Expand Down Expand Up @@ -112,6 +119,11 @@
}
}

.header-actions {
display: flex;
flex-direction: row-reverse;
}

.side-panel {
z-index: 4;
display: flex;
Expand All @@ -124,6 +136,12 @@
background-color: $gray-200;
border-right: 2px solid $gray-400;
box-shadow: 2px 0 4px -2px $transparent-200;

&.edit-mode-disabled {
opacity: 0.5;
pointer-events: none;
user-select: none;
}
}

.user-details {
Expand Down Expand Up @@ -241,6 +259,11 @@
gap: 2px;
justify-content: flex-start;
background-color: $gray-400;
grid-auto-flow: dense;
}

.products-container {
display: contents;
}

.product-grid-product {
Expand Down Expand Up @@ -279,4 +302,169 @@
grid-area: order-grid;
overflow-y: auto;
}

.folder-container {
display: contents;
}

.folder-tile {
position: relative;
background-color: $gray-600;
color: $white;

.folder-icon {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.5rem;
font-size: 3.5rem;
position: relative;
}

.folder-back-arrow {
position: absolute;
font-size: 1rem;
bottom: 0;
right: -0.5rem;
color: $white;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 0.2rem;
}

.product-grid-product-name {
color: $font-color-dark;
font-size: $font-size-lg;
text-shadow: none;
}

&.edit-mode {
cursor: grab;
}
}

.folder-edit-btn {
position: absolute;
top: 8px;
right: 8px;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
cursor: pointer;
font-size: 1.6rem;
color: $gray-700;
transition: all 0.2s ease;

&:hover {
background-color: $white;
transform: scale(1.1);
}
}

.add-folder-tile {
background-color: $gray-400;
border: 2px dashed $gray-600;

.folder-icon {
color: $gray-600;
}

.product-grid-product-name {
color: $gray-600;
}

&:hover {
background-color: $gray-300;
}
}

.drop-home-tile {
background-color: $gray-500;
border: 2px dashed $gray-700;
}

.back-button-tile {
pointer-events: auto;

.folder-icon {
font-size: 2rem;
}
}

.draggable {
cursor: grab;

&:active {
cursor: grabbing;
}
}

.drag-handle {
position: absolute;
top: 8px;
left: 8px;
color: rgba(0, 0, 0, 0.3);
font-size: 1rem;
}

.sortable-ghost {
opacity: 0.4;
}

.sortable-chosen {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.sortable-drag {
background-color: $white;
}

.folder-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1050;
}

.folder-modal {
background-color: $white;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

.folder-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid $gray-300;

h5 {
margin: 0;
}
}

.folder-modal-body {
padding: 1rem;
}

.folder-modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid $gray-300;
}
}
10 changes: 9 additions & 1 deletion app/controllers/activities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,21 @@ def order_screen # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
.find(params[:id])

@product_prices_json = sorted_product_price(@activity).to_json(
only: %i[id price position product_price_folder_id],
include: { product: { only: %i[id name category color], methods: %i[requires_age] } }
)

@folders_json = @activity.price_list.product_price_folders.order(:position).to_json(
only: %i[id name position color]
)

@users_json = users_hash.to_json

@activity_json = @activity.to_json(only: %i[id title start_time end_time])

@is_treasurer = current_user.treasurer?
@price_list_id = @activity.price_list_id

@sumup_key = Rails.application.config.x.sumup_key
@sumup_enabled = @sumup_key.present?

Expand Down Expand Up @@ -174,7 +182,7 @@ def users_hash
end

def sorted_product_price(activity)
activity.price_list.product_price.sort_by { |p| p.product.id }
activity.price_list.product_price.includes(:product).order(:position)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line uses the incorrect association name '.product_price' (singular) which should be '.product_prices' (plural) to match the corrected association in the PriceList model. This will cause a NoMethodError at runtime.

Suggested change
activity.price_list.product_price.includes(:product).order(:position)
activity.price_list.product_prices.includes(:product).order(:position)

Copilot uses AI. Check for mistakes.
end

def activity_params
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/errors_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def unacceptable
render_error_page('errors/unacceptable', :not_acceptable, '406 Not Acceptable')
end

def unprocessable_entity
render_error_page('errors/unprocessable_entity', :unprocessable_entity, '422 Unprocessable Entity')
def unprocessable_content
render_error_page('errors/unprocessable_content', :unprocessable_content, '422 Unprocessable Entity')
end

def internal_server_error
Expand Down
78 changes: 78 additions & 0 deletions app/controllers/product_price_folders_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
class ProductPriceFoldersController < ApplicationController
before_action :authenticate_user!
before_action :set_price_list, only: %i[index create reorder]
before_action :set_folder, only: %i[update destroy]

def index
authorize ProductPriceFolder, :index?
@folders = @price_list.product_price_folders.order(:position)
render json: @folders
end

def create
@folder = @price_list.product_price_folders.new(folder_params)
authorize @folder

if @folder.save
render json: @folder, status: :created
else
render json: { errors: @folder.errors.full_messages }, status: :unprocessable_content
end
end

def update
authorize @folder

if @folder.update(folder_params)
render json: @folder
else
render json: { errors: @folder.errors.full_messages }, status: :unprocessable_content
end
end

def destroy
authorize @folder

orphaned_products = @folder.product_prices
max_position = @folder.price_list.product_price.without_folder.maximum(:position) || -1
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line attempts to call '.product_price.without_folder' on the price_list, but the association name 'product_price' is singular and incorrect. The PriceList model (line 2) uses 'has_many :product_price' which should be 'has_many :product_prices' (plural). Once that is fixed, this line should be changed to '@folder.price_list.product_prices.without_folder.maximum(:position)' to match the corrected association name.

Suggested change
max_position = @folder.price_list.product_price.without_folder.maximum(:position) || -1
max_position = @folder.price_list.product_prices.without_folder.maximum(:position) || -1

Copilot uses AI. Check for mistakes.

orphaned_products.each_with_index do |product_price, index|
product_price.update(product_price_folder_id: nil, position: max_position + index + 1)
end

@folder.destroy

head :no_content
end
Comment on lines +33 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: product_price should be product_prices on line 37.

PriceList was updated (in app/models/price_list.rb, line 2) to use has_many :product_prices (plural). This line still uses the old singular name product_price, which will raise NoMethodError and break the entire destroy action.

Additionally, the individual product updates (lines 39–41) are not wrapped in a transaction, so a failure mid-way would leave partially reassigned products.

🐛 Proposed fix
   def destroy
     authorize `@folder`

     orphaned_products = `@folder.product_prices`
-    max_position = `@folder.price_list.product_price.without_folder.maximum`(:position) || -1
+    max_position = `@folder.price_list.product_prices.without_folder.maximum`(:position) || -1

-    orphaned_products.each_with_index do |product_price, index|
-      product_price.update(product_price_folder_id: nil, position: max_position + index + 1)
-    end
-
-    `@folder.destroy`
+    ActiveRecord::Base.transaction do
+      orphaned_products.each_with_index do |product_price, index|
+        product_price.update!(product_price_folder_id: nil, position: max_position + index + 1)
+      end
+      `@folder.destroy`!
+    end

     head :no_content
   end
🤖 Prompt for AI Agents
In `@app/controllers/product_price_folders_controller.rb` around lines 33 - 46,
The destroy action references the wrong association name and lacks transactional
safety: change the call from `@folder.price_list.product_price` to
`@folder.price_list.product_prices` (plural) when computing max_position, and wrap
the reassignment loop and `@folder.destroy` inside a single database transaction
(e.g., ApplicationRecord.transaction or PriceList.transaction) so that updating
orphaned_products (product_price updates) and deleting the folder are atomic and
roll back on failure.


def reorder # rubocop:disable Metrics/MethodLength
authorize ProductPriceFolder, :reorder?

folder_positions = params.require(:folder_positions)

ActiveRecord::Base.transaction do
folder_positions.each do |folder_data|
folder = @price_list.product_price_folders.find(folder_data[:id])
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reorder action only authorizes at the class level (ProductPriceFolder) but doesn't authorize each individual folder being reordered. While this might be acceptable since all folders belong to the same price list, consider adding authorization for each folder to ensure consistency with the ProductPricesController pattern (line 32 in product_prices_controller.rb), which authorizes each product_price individually in its reorder action.

Suggested change
folder = @price_list.product_price_folders.find(folder_data[:id])
folder = @price_list.product_price_folders.find(folder_data[:id])
authorize folder, :reorder?

Copilot uses AI. Check for mistakes.
folder.update!(position: folder_data[:position])
end
end

render json: { success: true }
rescue ActiveRecord::RecordInvalid => e
render json: { errors: [e.message] }, status: :unprocessable_content
end

private

def set_price_list
@price_list = PriceList.find(params[:price_list_id])
end

def set_folder
@folder = ProductPriceFolder.find(params[:id])
end

def folder_params
params.require(:product_price_folder).permit(:name, :color, :position)
end
end
Comment on lines +1 to +78
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ProductPriceFoldersController lacks test coverage. The codebase has comprehensive test coverage for other controllers (as evidenced by the spec/controllers directory), but there are no tests for this new controller. Tests should be added to cover the index, create, update, destroy, and reorder actions, including authorization checks for different user roles.

Copilot uses AI. Check for mistakes.
Loading
Loading