diff --git a/app/assets/stylesheets/order_screen.scss b/app/assets/stylesheets/order_screen.scss index ed42d879b..4d3164a83 100644 --- a/app/assets/stylesheets/order_screen.scss +++ b/app/assets/stylesheets/order_screen.scss @@ -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 { @@ -112,6 +119,11 @@ } } + .header-actions { + display: flex; + flex-direction: row-reverse; + } + .side-panel { z-index: 4; display: flex; @@ -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 { @@ -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 { @@ -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; + } } \ No newline at end of file diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index b8cb77909..458f8095e 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -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? @@ -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) end def activity_params diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index d82ca2e85..830792772 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -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 diff --git a/app/controllers/product_price_folders_controller.rb b/app/controllers/product_price_folders_controller.rb new file mode 100644 index 000000000..46bda4065 --- /dev/null +++ b/app/controllers/product_price_folders_controller.rb @@ -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 + + 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 + + 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]) + 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 diff --git a/app/controllers/product_prices_controller.rb b/app/controllers/product_prices_controller.rb new file mode 100644 index 000000000..29d5a5f8d --- /dev/null +++ b/app/controllers/product_prices_controller.rb @@ -0,0 +1,58 @@ +class ProductPricesController < ApplicationController + before_action :authenticate_user! + before_action :set_product_price, only: %i[assign_folder] + before_action :set_price_list, only: %i[reorder] + + def assign_folder # rubocop:disable Metrics/MethodLength + authorize @product_price, :update? + + folder_id = params[:folder_id] + + if folder_id.present? + folder = ProductPriceFolder.find_by(id: folder_id) + return render json: { errors: ['Folder not found'] }, status: :unprocessable_content unless folder + unless folder.price_list_id == @product_price.price_list_id + return render json: { errors: ['Folder does not belong to the same price list'] }, status: :unprocessable_content + end + end + + if @product_price.update(product_price_folder_id: folder_id) + render json: @product_price, include: product_price_includes + else + render json: { errors: @product_price.errors.full_messages }, status: :unprocessable_content + end + end + + def reorder # rubocop:disable Metrics/MethodLength + product_positions = params.require(:product_positions) + + ActiveRecord::Base.transaction do + product_positions.each do |product_data| + product_price = @price_list.product_prices.find(product_data[:id]) + authorize product_price, :update? + product_price.update!( + position: product_data[:position], + product_price_folder_id: product_data[:folder_id] + ) + end + end + + render json: { success: true } + rescue ActiveRecord::RecordInvalid => e + render json: { errors: [e.message] }, status: :unprocessable_content + end + + private + + def set_product_price + @product_price = ProductPrice.find(params[:id]) + end + + def set_price_list + @price_list = PriceList.find(params[:price_list_id]) + end + + def product_price_includes + { product: { only: %i[id name category color], methods: %i[requires_age] } } + end +end diff --git a/app/javascript/order_screen.js b/app/javascript/order_screen.js index f825a479f..8e1b1ab8d 100644 --- a/app/javascript/order_screen.js +++ b/app/javascript/order_screen.js @@ -1,6 +1,7 @@ import Vue from 'vue/dist/vue.esm'; import api from './api/axiosInstance'; import * as bootstrap from 'bootstrap'; +import Sortable from 'sortablejs'; import FlashNotification from './components/FlashNotification.vue'; import UserSelection from './components/orderscreen/UserSelection.vue'; @@ -11,9 +12,12 @@ document.addEventListener('turbo:load', () => { if (element != null) { const users = JSON.parse(element.dataset.users); const productPrices = JSON.parse(element.dataset.productPrices); + const folders = JSON.parse(element.dataset.folders || '[]'); const activity = JSON.parse(element.dataset.activity); const flashes = JSON.parse(element.dataset.flashes); const depositButtonEnabled = element.dataset.depositButtonEnabled === 'true'; + const isTreasurer = element.dataset.isTreasurer === 'true'; + const priceListId = element.dataset.priceListId; window.flash = function(message, actionText, type) { const event = new CustomEvent('flash', { detail: { message: message, actionText: actionText, type: type } } ); @@ -32,6 +36,7 @@ document.addEventListener('turbo:load', () => { return { users: users, productPrices: productPrices, + folders: folders, activity: activity, selectedUser: null, payWithCash: false, @@ -39,7 +44,16 @@ document.addEventListener('turbo:load', () => { keepUserSelected: false, depositButtonEnabled: depositButtonEnabled, orderRows: [], - isSubmitting: false + isSubmitting: false, + currentFolder: null, + editMode: false, + isTreasurer: isTreasurer, + priceListId: priceListId, + showFolderModal: false, + editingFolder: null, + folderForm: { name: '', color: '#6c757d' }, + draggedItem: null, + sortableInstance: null }; }, methods: { @@ -51,13 +65,32 @@ document.addEventListener('turbo:load', () => { return `€${parseFloat(price).toFixed(2)}`; }, + isValidHexColor(color) { + return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color); + }, + + leaveOrderScreen() { + if (this.payWithCash) { + this.payWithCash = false; + } else if (this.payWithPin) { + this.payWithPin = false; + } else { + this.selectedUser = null; + } + + this.currentFolder = null; + if (this.editMode) { + this.editMode = false; + this.destroySortable(); + } + }, + setUser(user = null) { if (this.selectedUser === null || user === null || this.selectedUser.id != user.id) { this.orderRows = []; } if (user !== null) { - // Reload user to get latest credit balance api.get(`/users/${user.id}/json?activity_id=${this.activity.id}`).then((response) => { const refreshedUser = response.data; const index = this.users.findIndex((candidate) => candidate.id === refreshedUser.id); @@ -78,6 +111,14 @@ document.addEventListener('turbo:load', () => { this.payWithCash = false; this.payWithPin = false; this.selectedUser = user; + + if (user === null) { + this.currentFolder = null; + if (this.editMode) { + this.editMode = false; + this.destroySortable(); + } + } }, selectCash() { @@ -182,7 +223,6 @@ document.addEventListener('turbo:load', () => { if(!this.keepUserSelected){ this.setUser(null); } else { - // re-set user to update credit this.setUser(response.data.user); this.orderRows = []; } @@ -247,6 +287,267 @@ document.addEventListener('turbo:load', () => { this.handleXHRError(response); }); }, + + enterFolder(folder) { + if (!this.editMode) { + this.currentFolder = folder; + } + }, + + exitFolder() { + if (!this.editMode) { + this.currentFolder = null; + } + }, + + toggleEditMode() { + this.editMode = !this.editMode; + if (this.editMode) { + this.$nextTick(() => { + this.initSortable(); + }); + } else { + this.destroySortable(); + } + }, + + initSortable() { + const productsContainer = this.$refs.productsContainer; + if (productsContainer && !this.sortableInstance) { + this.sortableInstance = Sortable.create(productsContainer, { + animation: 100, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + forceFallback: false, + touchStartThreshold: 0, + delayOnTouchOnly: true, + delay: 50, + onEnd: this.onProductDragEnd.bind(this) + }); + } + + const foldersContainer = this.$refs.foldersContainer; + if (foldersContainer && !this.folderSortableInstance) { + this.folderSortableInstance = Sortable.create(foldersContainer, { + animation: 100, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + forceFallback: false, + touchStartThreshold: 0, + delayOnTouchOnly: true, + delay: 50, + onEnd: this.onFolderDragEnd.bind(this) + }); + } + }, + + destroySortable() { + if (this.sortableInstance) { + this.sortableInstance.destroy(); + this.sortableInstance = null; + } + if (this.folderSortableInstance) { + this.folderSortableInstance.destroy(); + this.folderSortableInstance = null; + } + }, + + onProductDragEnd(evt) { + const productPositions = []; + const productElements = evt.to.querySelectorAll('[data-product-price-id]'); + productElements.forEach((el, index) => { + const productPriceId = el.dataset.productPriceId; + if (productPriceId) { + productPositions.push({ + id: parseInt(productPriceId), + position: index, + folder_id: this.currentFolder ? this.currentFolder.id : null + }); + const productPrice = this.productPrices.find(p => p.id == productPriceId); + if (productPrice) productPrice.position = index; + } + }); + + if (productPositions.length > 0) { + api.patch(`/price_lists/${this.priceListId}/product_prices/reorder`, { + product_positions: productPositions + }).catch((response) => { + this.handleXHRError(response); + }); + } + }, + + onFolderDragEnd(evt) { + const folderPositions = []; + const folderElements = evt.to.querySelectorAll('.folder-tile'); + folderElements.forEach((el, index) => { + const folderId = el.dataset.folderId; + if (folderId) { + folderPositions.push({ id: parseInt(folderId), position: index }); + const folder = this.folders.find(f => f.id == folderId); + if (folder) folder.position = index; + } + }); + + if (folderPositions.length > 0) { + api.patch(`/price_lists/${this.priceListId}/product_price_folders/reorder`, { + folder_positions: folderPositions + }).catch((response) => { + this.handleXHRError(response); + }); + } + }, + + assignProductToFolder(productPrice, folderId, newPosition = 0) { + api.patch(`/product_prices/${productPrice.id}/assign_folder`, { + folder_id: folderId + }).then(() => { + productPrice.product_price_folder_id = folderId ? parseInt(folderId) : null; + productPrice.position = newPosition; + }).catch((response) => { + this.handleXHRError(response); + }); + }, + + onDropOnFolder(evt, folder) { + evt.preventDefault(); + evt.stopPropagation(); + if (!this.draggedItem || !folder) return; + + const productPrice = this.draggedItem; + const folderId = parseInt(folder.id); + + const productsInFolder = this.productPrices.filter(pp => pp.product_price_folder_id == folderId); + let maxPosition = -1; + productsInFolder.forEach(pp => { + if (typeof pp.position === 'number' && pp.position > maxPosition) { + maxPosition = pp.position; + } + + const productPrice = this.draggedItem; + const folderId = parseInt(folder.id); + + // Determine the next available position within the target folder + const productsInFolder = this.productPrices.filter(pp => pp.product_price_folder_id == folderId); + let maxPosition = -1; + productsInFolder.forEach(pp => { + if (typeof pp.position === 'number' && pp.position > maxPosition) { + maxPosition = pp.position; + } + }); + + productPrice.product_price_folder_id = folderId; + productPrice.position = maxPosition + 1; + api.patch(`/product_prices/${productPrice.id}/assign_folder`, { + folder_id: folder.id + }).catch((response) => { + this.handleXHRError(response); + }); + }, + + onDropOnBackButton(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (!this.draggedItem) return; + + const productPrice = this.draggedItem; + productPrice.product_price_folder_id = null; + const rootProducts = this.productPrices.filter(pp => !pp.product_price_folder_id); + productPrice.position = rootProducts.length > 0 ? rootProducts.length - 1 : 0; + + api.patch(`/product_prices/${productPrice.id}/assign_folder`, { + folder_id: null + }).catch((response) => { + this.handleXHRError(response); + }); + }, + + onDragStartProduct(evt, productPrice) { + this.draggedItem = productPrice; + }, + + onDragEnd() { + this.draggedItem = null; + }, + + openFolderModal(folder = null) { + this.editingFolder = folder; + if (folder) { + this.folderForm = { name: folder.name, color: folder.color }; + } else { + this.folderForm = { name: '', color: '#6c757d' }; + } + this.showFolderModal = true; + }, + + closeFolderModal() { + this.showFolderModal = false; + this.editingFolder = null; + this.folderForm = { name: '', color: '#6c757d' }; + }, + + saveFolder() { + if (!this.folderForm.name.trim()) { + this.sendFlash('Voer een mapnaam in', '', 'warning'); + return; + } + + const normalizedColor = (this.folderForm.color || '').trim(); + if (!this.isValidHexColor(normalizedColor)) { + this.sendFlash('Voer een geldige hex kleur in (bijv. #6c757d)', '', 'warning'); + return; + } + this.folderForm.color = normalizedColor; + + if (this.editingFolder) { + api.patch(`/product_price_folders/${this.editingFolder.id}`, { + product_price_folder: this.folderForm + }).then((response) => { + const index = this.folders.findIndex(f => f.id === this.editingFolder.id); + if (index !== -1) { + this.$set(this.folders, index, response.data); + } + this.sendFlash('Map bijgewerkt', '', 'success'); + this.closeFolderModal(); + }).catch((response) => { + this.handleXHRError(response); + }); + } else { + api.post(`/price_lists/${this.priceListId}/product_price_folders`, { + product_price_folder: this.folderForm + }).then((response) => { + this.folders.push(response.data); + this.sendFlash('Map aangemaakt', '', 'success'); + this.closeFolderModal(); + }).catch((response) => { + this.handleXHRError(response); + }); + } + }, + + deleteFolder(folder) { + if (!confirm(`Map "${folder.name}" verwijderen? Producten worden terug naar het hoofdscherm verplaatst.`)) { + return; + } + + api.delete(`/product_price_folders/${folder.id}`).then(() => { + this.productPrices.forEach(pp => { + if (pp.product_price_folder_id === folder.id) { + pp.product_price_folder_id = null; + } + }); + const index = this.folders.findIndex(f => f.id === folder.id); + if (index !== -1) { + this.folders.splice(index, 1); + } + this.sendFlash('Map verwijderd', '', 'success'); + this.closeFolderModal(); + }).catch((response) => { + this.handleXHRError(response); + }); + }, }, computed: { @@ -295,6 +596,34 @@ document.addEventListener('turbo:load', () => { isMobile() { return this.isIos || /Android|webOS|Opera Mini/i.test(navigator.userAgent); + }, + + sortedFolders() { + return [...this.folders].sort((a, b) => a.position - b.position); + }, + + productsWithoutFolder() { + return this.productPrices + .filter(pp => !pp.product_price_folder_id) + .sort((a, b) => a.position - b.position); + }, + + productsInCurrentFolder() { + if (!this.currentFolder) return []; + return this.productPrices + .filter(pp => pp.product_price_folder_id === this.currentFolder.id) + .sort((a, b) => a.position - b.position); + }, + + visibleProducts() { + if (this.currentFolder) { + return this.productsInCurrentFolder; + } + return this.productsWithoutFolder; + }, + + isInFolder() { + return this.currentFolder !== null; } }, diff --git a/app/models/price_list.rb b/app/models/price_list.rb index 42e050344..ef6cb42fc 100644 --- a/app/models/price_list.rb +++ b/app/models/price_list.rb @@ -1,7 +1,8 @@ class PriceList < ApplicationRecord - has_many :product_price, dependent: :destroy - has_many :products, through: :product_price, dependent: :restrict_with_exception + has_many :product_prices, dependent: :destroy + has_many :products, through: :product_prices, dependent: :restrict_with_exception has_many :activities, dependent: :restrict_with_exception + has_many :product_price_folders, dependent: :destroy validates :name, presence: true diff --git a/app/models/product_price.rb b/app/models/product_price.rb index 1afb21990..d46e93c4b 100644 --- a/app/models/product_price.rb +++ b/app/models/product_price.rb @@ -1,9 +1,31 @@ class ProductPrice < ApplicationRecord + + belongs_to :product belongs_to :price_list + belongs_to :product_price_folder, optional: true validates :price, presence: true, inclusion: { in: 0..100 } validates :product_id, uniqueness: { scope: %i[price_list_id deleted_at] } + validates :position, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } delegate :name, to: :product + + scope :ordered, -> { order(:position) } + + before_validation :set_default_position, on: :create + + scope :without_folder, -> { where(product_price_folder_id: nil) } + + scope :in_folder, ->(folder) { where(product_price_folder: folder) } + + private + + def set_default_position + return if position.present? && position >= 0 + + scope = price_list&.product_prices&.where(product_price_folder_id: product_price_folder_id) + max_position = scope&.maximum(:position) || -1 + self.position = max_position + 1 + end end diff --git a/app/models/product_price_folder.rb b/app/models/product_price_folder.rb new file mode 100644 index 000000000..bdbabc590 --- /dev/null +++ b/app/models/product_price_folder.rb @@ -0,0 +1,23 @@ +class ProductPriceFolder < ApplicationRecord + + belongs_to :price_list + has_many :product_prices, dependent: :nullify + + validates :name, presence: true + validates :color, presence: true, + format: { with: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, message: 'must be a valid hexcode (e.g., #FF5733 or #F57)' } + validates :position, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + default_scope { order(:position) } + + before_validation :set_default_position, on: :create + + private + + def set_default_position + return if position.present? && position.positive? + + max_position = price_list&.product_price_folders&.maximum(:position) || -1 + self.position = max_position + 1 + end +end diff --git a/app/policies/product_price_folder_policy.rb b/app/policies/product_price_folder_policy.rb new file mode 100644 index 000000000..705828e66 --- /dev/null +++ b/app/policies/product_price_folder_policy.rb @@ -0,0 +1,31 @@ +class ProductPriceFolderPolicy < ApplicationPolicy + def index? + user&.treasurer? || user&.renting_manager? || user&.main_bartender? + end + + def show? + index? + end + + def create? + user&.treasurer? + end + + def update? + user&.treasurer? + end + + def destroy? + user&.treasurer? + end + + def reorder? + user&.treasurer? + end + + class Scope < ApplicationPolicy::Scope + def resolve + scope.all + end + end +end diff --git a/app/policies/product_price_policy.rb b/app/policies/product_price_policy.rb index c813a1eb0..9da225ff8 100644 --- a/app/policies/product_price_policy.rb +++ b/app/policies/product_price_policy.rb @@ -1,5 +1,23 @@ class ProductPricePolicy < ApplicationPolicy + def update? + user&.treasurer? + end + def destroy? user&.treasurer? end + + def assign_folder? + update? + end + + def reorder? + update? + end + + class Scope < ApplicationPolicy::Scope + def resolve + scope.all + end + end end diff --git a/app/views/activities/order_screen.html.erb b/app/views/activities/order_screen.html.erb index ebd8b2c2f..dedfe0a22 100644 --- a/app/views/activities/order_screen.html.erb +++ b/app/views/activities/order_screen.html.erb @@ -3,7 +3,7 @@ <%= javascript_include_tag "order_screen", "data-turbo-track": "reload", defer: true %> <% end %> <%= content_tag :div, id: 'order-screen', class: 'order-screen', - data: {users: @users_json, product_prices: @product_prices_json, activity: @activity_json, sumup_callback: sumup_callback_activity_url, sumup_key: @sumup_key, flashes: flash, site_name: Rails.application.config.x.site_short_name, deposit_button_enabled: Rails.application.config.x.deposit_button_enabled} do + data: {users: @users_json, product_prices: @product_prices_json, folders: @folders_json, activity: @activity_json, is_treasurer: @is_treasurer, price_list_id: @price_list_id, sumup_callback: sumup_callback_activity_url, sumup_key: @sumup_key, flashes: flash, site_name: Rails.application.config.x.site_short_name, deposit_button_enabled: Rails.application.config.x.deposit_button_enabled} do %> @@ -18,11 +18,11 @@ -
+
@@ -66,11 +71,11 @@