From 63eb64b239d97907d70d0ff13813398b13dda70e Mon Sep 17 00:00:00 2001 From: puppe1990 Date: Mon, 28 Oct 2024 19:42:53 -0300 Subject: [PATCH 1/2] create a qr code reader --- app/assets/stylesheets/qr_scanner.css | 133 +++++++++++++ app/controllers/api/v1/products_controller.rb | 41 +++- app/controllers/products_controller.rb | 36 +++- app/controllers/qr_scanner_controller.rb | 5 + .../services/product/generate_qr_code.rb | 2 +- app/serializers/product_serializer.rb | 37 ++-- app/services/product/generate_qr_code.rb | 15 ++ app/views/products/index.html.erb | 6 +- app/views/products/scan_qr_code.html.erb | 72 +++++++ app/views/qr_scanner/index.html.erb | 186 ++++++++++++++++++ config/routes.rb | 6 + 11 files changed, 511 insertions(+), 28 deletions(-) create mode 100644 app/assets/stylesheets/qr_scanner.css create mode 100644 app/controllers/qr_scanner_controller.rb create mode 100644 app/services/product/generate_qr_code.rb create mode 100644 app/views/products/scan_qr_code.html.erb create mode 100644 app/views/qr_scanner/index.html.erb diff --git a/app/assets/stylesheets/qr_scanner.css b/app/assets/stylesheets/qr_scanner.css new file mode 100644 index 00000000..4277a7ae --- /dev/null +++ b/app/assets/stylesheets/qr_scanner.css @@ -0,0 +1,133 @@ +#reader { + width: 100%; + max-width: 600px; + margin: 0 auto; + z-index: 1; + position: relative; +} + +#raw-data { + background: #f8f9fa; + border-radius: 4px; + padding: 10px; + word-break: break-all; +} + +.modal-body pre { + max-height: 200px; + overflow-y: auto; +} + +/* Remove modal backdrop and adjust modal styling */ +.modal-backdrop { + display: none !important; +} + +.modal { + background: rgba(0, 0, 0, 0.5); + z-index: 1050 !important; +} + +.modal-dialog { + z-index: 1051 !important; + position: relative; + margin-top: 2rem; +} + +.modal-content { + position: relative; + box-shadow: 0 5px 15px rgba(0,0,0,.5); + border: 1px solid rgba(0,0,0,.2); + border-radius: 12px; + border: none; + box-shadow: 0 5px 15px rgba(0,0,0,0.2); +} + +.modal-header { + border-radius: 12px 12px 0 0; + padding: 1.2rem; +} + +.modal-body { + padding: 1.5rem; +} + +.product-info-card { + background: #fff; + border-radius: 8px; + padding: 1.5rem; +} + +.product-header { + text-align: center; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; +} + +.product-header h4 { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + color: #2d3748; +} + +.info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.info-item { + display: flex; + align-items: flex-start; + gap: 1rem; +} + +.info-item i { + font-size: 1.2rem; + padding-top: 0.2rem; +} + +.info-content { + flex: 1; +} + +.info-content label { + display: block; + font-size: 0.875rem; + color: #718096; + margin-bottom: 0.25rem; +} + +.info-content .value { + font-size: 1rem; + font-weight: 500; + color: #2d3748; +} + +.action-buttons { + margin-top: 2rem; +} + +.action-buttons .btn { + padding: 0.75rem 1.5rem; + font-weight: 500; +} + +.modal-footer { + border-radius: 0 0 12px 12px; + padding: 1rem 1.5rem; +} + +/* Hide the HTML5 QR scanner's continue button when modal is shown */ +.modal.show ~ #reader__dashboard_section_csr button { + display: none !important; +} + +/* Ensure scanner elements don't overlap modal */ +#reader__dashboard_section_csr, +#reader__dashboard_section_swaplink, +#reader__scan_region { + z-index: 1 !important; +} \ No newline at end of file diff --git a/app/controllers/api/v1/products_controller.rb b/app/controllers/api/v1/products_controller.rb index 19300d36..ee6843f5 100644 --- a/app/controllers/api/v1/products_controller.rb +++ b/app/controllers/api/v1/products_controller.rb @@ -2,17 +2,54 @@ module Api module V1 - class ProductsController < ApplicationController + class ProductsController < ActionController::API + include ActionController::MimeResponds + set_current_tenant_through_filter + before_action :set_tenant_from_qr_data + + def show_by_id + begin + @product = Product.find_by!(id: params[:id]) + render json: ProductSerializer.new(@product).serializable_hash + rescue ActiveRecord::RecordNotFound => e + Rails.logger.error "Product not found: #{e.message}" + render json: { error: 'Product not found' }, status: :not_found + rescue StandardError => e + Rails.logger.error "Error in show_by_id: #{e.message}\n#{e.backtrace.join("\n")}" + render json: { error: 'Internal server error' }, status: :internal_server_error + end + end + def show - @product = Product.find_by(custom_id: params[:custom_id]) + @product = Product.find_by!(id: params[:id]) + render json: ProductSerializer.new(@product).serializable_hash + rescue ActiveRecord::RecordNotFound => e + render json: { error: 'Product not found' }, status: :not_found end def show_product @product = Product.find(params[:id]) + render json: ProductSerializer.new(@product).serializable_hash end def index @products = Product.where(active: true) + render json: ProductSerializer.new(@products).serializable_hash + end + + def find_by_sku + @product = Product.find_by!(sku: params[:sku]) + render json: ProductSerializer.new(@product).serializable_hash + rescue ActiveRecord::RecordNotFound + render json: { error: 'Product not found' }, status: :not_found + end + + private + + def set_tenant_from_qr_data + account_id = request.headers['X-Account-ID'] || params[:account_id] + current_account = Account.find_by(id: account_id) + set_current_tenant(current_account) end end end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index bbf23307..57ac049c 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -150,9 +150,39 @@ def update_active def download_qr_code @product = Product.find(params[:id]) - qr_code = RQRCode::QRCode.new(@product.sku) - png = qr_code.as_png(size: 300) - send_data png, type: 'image/png', disposition: 'attachment', filename: "#{@product.name}_qr_code.png" + qr_code_data_url = Services::Product::GenerateQrCode.new(product: @product).call + + # Convert data URL to binary + png_data = Base64.decode64(qr_code_data_url.split(',')[1]) + + send_data png_data, + type: 'image/png', + disposition: 'attachment', + filename: "#{@product.name}_qr_code.png" + end + + def scan_qr_code + # Just renders the view with the QR scanner + end + + def update_stock_from_qr + @product = Product.find(params[:id]) + quantity = params[:quantity].to_i + + begin + ActiveRecord::Base.transaction do + # Create a new stock record or update existing + if @product.stock.nil? + @product.create_stock(quantity: quantity) + else + @product.stock.update!(quantity: quantity) + end + + render json: { success: true, message: 'Stock updated successfully' } + end + rescue StandardError => e + render json: { success: false, error: e.message }, status: :unprocessable_entity + end end private diff --git a/app/controllers/qr_scanner_controller.rb b/app/controllers/qr_scanner_controller.rb new file mode 100644 index 00000000..3087e60e --- /dev/null +++ b/app/controllers/qr_scanner_controller.rb @@ -0,0 +1,5 @@ +class QrScannerController < ApplicationController + def index + # Just renders the view + end +end \ No newline at end of file diff --git a/app/models/services/product/generate_qr_code.rb b/app/models/services/product/generate_qr_code.rb index 45ca0d9b..19730476 100644 --- a/app/models/services/product/generate_qr_code.rb +++ b/app/models/services/product/generate_qr_code.rb @@ -12,7 +12,7 @@ def initialize(product:, width: 300, height: 300) end def call - object = { id: @product.id, custom_id: @product.custom_id, name: @product.name } + object = { id: @product.id, account_id: @product.account_id } RQRCode::QRCode.new(object.to_json).to_img.resize(@width, @height).to_data_url end end diff --git a/app/serializers/product_serializer.rb b/app/serializers/product_serializer.rb index ef89d8bd..8d7f3c3a 100644 --- a/app/serializers/product_serializer.rb +++ b/app/serializers/product_serializer.rb @@ -31,33 +31,28 @@ # fk_rails_... (category_id => categories.id) # class ProductSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer - attributes :id, :custom_id, :name, :count_purchase_product, :count_sale_product, :active, :sku - - attribute :price do |object| - "R$ #{object.price}" - end - - attribute :balance, &:balance - - attribute :custom_id do |object| - object.custom_id.to_i - end + attributes :id, :name, :sku, :price, :active, :custom_id attribute :category do |object| - object.category.name + object.category&.name end - attribute :image_url do |object| - object.image.attached? ? Rails.application.routes.url_helpers.rails_blob_path(object.image, only_path: true) : 'https://purple-stock.s3-sa-east-1.amazonaws.com/images.png' - end - - attribute :active do |object| - if object.active - 'Sim' + attribute :stock do |object| + if object.stock + object.stock.try(:quantity) || 0 else - 'Não' + 0 end end + + attribute :formatted_price do |object| + ActionController::Base.helpers.number_to_currency( + object.price, + unit: "R$", + separator: ",", + delimiter: "." + ) + end end diff --git a/app/services/product/generate_qr_code.rb b/app/services/product/generate_qr_code.rb new file mode 100644 index 00000000..100f734b --- /dev/null +++ b/app/services/product/generate_qr_code.rb @@ -0,0 +1,15 @@ +module Services + module Product + class GenerateQrCode + def initialize(product:) + @product = product + end + + def call + qr_data = { id: @product.id, account_id: @product.account_id }.to_json + qr_code = RQRCode::QRCode.new(qr_data) + qr_code.as_png(size: 50).to_data_url + end + end + end +end \ No newline at end of file diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb index ada5b02f..419b20e6 100644 --- a/app/views/products/index.html.erb +++ b/app/views/products/index.html.erb @@ -17,6 +17,7 @@ <% end %> <%= link_to t('.new', :default => t("helpers.links.new")), new_product_path, :class => 'btn btn-primary mb-3' %> + <%= link_to icon('fas fa-camera') + ' Scan QR Code', qr_scanner_path, class: 'btn btn-info mb-3 ml-2' %>
@@ -43,7 +44,10 @@ <%= tag.tr id: dom_id(product) do %> <%= link_to (product.image.attached? ? image_tag(rails_blob_path(product.image.variant(:thumb)), class: "thumb-image #{'thumb-image-mobile' if (platform.mobile? || platform.mobile_app?) }") : image_tag('products/product-5-50.png', size: '50', class: "thumb-image #{'thumb-image-mobile' if (platform.mobile? || platform.mobile_app?) }" )), product_path(product), data: { toggle: 'tooltip', turbo: false } %> <%= product.name %> - <%= product.sku %> + + <%= product.sku %> + <%= image_tag Services::Product::GenerateQrCode.new(product: product).call, size: '50' %> + <%= product.custom_id %> <%= number_to_currency_pt_br(product.price) %> <%= Services::Product::CountQuantity.call(product: product, product_command: 'purchase_product') %> diff --git a/app/views/products/scan_qr_code.html.erb b/app/views/products/scan_qr_code.html.erb new file mode 100644 index 00000000..02742923 --- /dev/null +++ b/app/views/products/scan_qr_code.html.erb @@ -0,0 +1,72 @@ +
+ +
+ +
+
+
+
+ + +
+
+
+ +<%= javascript_include_tag "https://unpkg.com/html5-qrcode" %> + + \ No newline at end of file diff --git a/app/views/qr_scanner/index.html.erb b/app/views/qr_scanner/index.html.erb new file mode 100644 index 00000000..90b42aa3 --- /dev/null +++ b/app/views/qr_scanner/index.html.erb @@ -0,0 +1,186 @@ +
+ +
+ +
+
+
+
+
+
+
+ + + + +<%= javascript_include_tag "https://unpkg.com/html5-qrcode" %> + + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 3e178e49..3e522b96 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,6 +86,8 @@ member do delete :destroy_from_index get 'download_qr_code' + get 'scan_qr_code' # Page with camera scanner + post 'update_stock_from_qr' # Endpoint to update stock end end resources :bling_data, only: %i[index show] @@ -142,6 +144,8 @@ post 'sale_products/remove_products', to: 'sale_products#remove_products', as: 'remove_products' post 'purchase_products/add_inventory_quantity', to: 'purchase_products#add_inventory_quantity', as: 'add_inventory_quantity' + get 'products/find_by_sku/:sku', to: 'products#find_by_sku', as: 'find_product_by_sku' + get 'products/:id/show_by_id', to: 'products#show_by_id', as: 'show_product_by_id' end end root to: 'home#index' @@ -174,4 +178,6 @@ resources :bling_module_situations + get 'qr_scanner', to: 'qr_scanner#index', as: 'qr_scanner' + end From b1b78bab9cf92432ea65dcce4b9770f91bddbdf4 Mon Sep 17 00:00:00 2001 From: puppe1990 Date: Mon, 28 Oct 2024 20:15:06 -0300 Subject: [PATCH 2/2] remove unitilized service --- app/services/product/generate_qr_code.rb | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 app/services/product/generate_qr_code.rb diff --git a/app/services/product/generate_qr_code.rb b/app/services/product/generate_qr_code.rb deleted file mode 100644 index 100f734b..00000000 --- a/app/services/product/generate_qr_code.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Services - module Product - class GenerateQrCode - def initialize(product:) - @product = product - end - - def call - qr_data = { id: @product.id, account_id: @product.account_id }.to_json - qr_code = RQRCode::QRCode.new(qr_data) - qr_code.as_png(size: 50).to_data_url - end - end - end -end \ No newline at end of file