diff --git a/Gemfile b/Gemfile index 530084b..e22afcf 100644 --- a/Gemfile +++ b/Gemfile @@ -108,6 +108,9 @@ gem "prawn", "2.4.0" # PDF generation gem "prawn-table" # Table support for prawn gem "ttfunk", "1.7.0" # TrueType font parsing (prawn dependency) +# Export formats +gem "caxlsx" # Excel XLSX generation (community-maintained axlsx successor) + # API and integrations gem "httparty" # HTTP client for API calls diff --git a/app/controllers/oroshi/exports_controller.rb b/app/controllers/oroshi/exports_controller.rb new file mode 100644 index 0000000..cbea528 --- /dev/null +++ b/app/controllers/oroshi/exports_controller.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class Oroshi::ExportsController < Oroshi::ApplicationController + before_action :authorize_export + + EXPORT_TYPES = { + "orders" => "Exports::OrdersExport", + "revenue" => "Exports::RevenueExport", + "production" => "Exports::ProductionExport", + "inventory" => "Exports::InventoryExport", + "supply" => "Exports::SupplyExport", + "shipping" => "Exports::ShippingExport" + }.freeze + + VALID_FORMATS = %w[csv xlsx json pdf].freeze + + # POST /oroshi/exports + def create + unless EXPORT_TYPES.key?(params[:export_type]) + return head :unprocessable_entity + end + + unless VALID_FORMATS.include?(params[:format_type]) + return head :unprocessable_entity + end + + message = create_export_message + Oroshi::ExportJob.perform_later( + export_class_name, + params[:format_type], + message.id, + export_options + ) + + head :ok + end + + private + + def export_class_name + EXPORT_TYPES.fetch(params[:export_type]) + end + + def export_options + params.permit( + :date, :start_date, :end_date, + :shipping_organization_id, :print_empty_buyers, + buyer_ids: [], shipping_method_ids: [], + order_category_ids: [], buyer_category_ids: [] + ).to_h.compact_blank + end + + def create_export_message + Message.create!( + user: current_user.id, + model: "oroshi_export", + state: nil, + message: I18n.t("oroshi.exports.processing"), + data: { + export_type: params[:export_type], + format: params[:format_type], + expiration: (DateTime.now + 1.day) + } + ) + end + + def authorize_export + authorize :export, :create? + end +end diff --git a/app/helpers/oroshi/exports_helper.rb b/app/helpers/oroshi/exports_helper.rb new file mode 100644 index 0000000..8409413 --- /dev/null +++ b/app/helpers/oroshi/exports_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Oroshi + module ExportsHelper + FORMAT_ICONS = { + "csv" => "filetype-csv", + "xlsx" => "file-earmark-spreadsheet", + "pdf" => "filetype-pdf", + "json" => "filetype-json" + }.freeze + + def export_format_icon(format) + icon(FORMAT_ICONS.fetch(format, "file-earmark")) + end + end +end diff --git a/app/jobs/oroshi/export_job.rb b/app/jobs/oroshi/export_job.rb new file mode 100644 index 0000000..606588a --- /dev/null +++ b/app/jobs/oroshi/export_job.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Oroshi::ExportJob < ApplicationJob + queue_as :default + + # @param export_class [String] fully qualified class name (e.g., "Exports::OrdersExport") + # @param format [String] export format ("csv", "xlsx", "json", "pdf") + # @param message_id [Integer] Message record ID for status tracking + # @param options [Hash] export options (date, filters, etc.) + def perform(export_class, format, message_id, options = {}) + message = Message.find(message_id) + + exporter = export_class.constantize.new(options) + content = exporter.generate(format) + + io = StringIO.new(content) + message.stored_file.attach( + io: io, + content_type: exporter.content_type(format), + filename: exporter.filename(format) + ) + + message.update(state: true, message: I18n.t("oroshi.exports.completed")) + + # Free memory after PDF generation (matches existing pattern) + GC.start if format.to_s == "pdf" + rescue StandardError => e + message&.update(state: false, message: I18n.t("oroshi.exports.failed", error: e.message)) + raise + end +end diff --git a/app/policies/oroshi/export_policy.rb b/app/policies/oroshi/export_policy.rb new file mode 100644 index 0000000..b4a7d1b --- /dev/null +++ b/app/policies/oroshi/export_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Oroshi + class ExportPolicy < ApplicationPolicy + # Export access mirrors order read access: + # Admin, VIP, and Employee can export data + def create? + user.admin? || user.vip? || user.employee? + end + end +end diff --git a/app/views/oroshi/exports/_date_range_export.html.erb b/app/views/oroshi/exports/_date_range_export.html.erb new file mode 100644 index 0000000..323d3d6 --- /dev/null +++ b/app/views/oroshi/exports/_date_range_export.html.erb @@ -0,0 +1,47 @@ +<%# Date range export modal %> +<%# locals: (export_type:) %> +
diff --git a/app/views/oroshi/exports/_export_button.html.erb b/app/views/oroshi/exports/_export_button.html.erb new file mode 100644 index 0000000..546e91b --- /dev/null +++ b/app/views/oroshi/exports/_export_button.html.erb @@ -0,0 +1,31 @@ +<%# Reusable export dropdown button for dashboard views %> +<%# locals: (export_type:, date:, additional_params: {}) %> +