Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 70 additions & 0 deletions app/controllers/oroshi/exports_controller.rb
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions app/helpers/oroshi/exports_helper.rb
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions app/jobs/oroshi/export_job.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/policies/oroshi/export_policy.rb
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions app/views/oroshi/exports/_date_range_export.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<%# Date range export modal %>
<%# locals: (export_type:) %>
<div class="modal fade" id="date_range_export_modal_<%= export_type %>" tabindex="-1"
aria-labelledby="date_range_export_label_<%= export_type %>" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<%= form_with url: exports_path, method: :post, data: { turbo_prefetch: false } do |f| %>
<%= f.hidden_field :export_type, value: export_type %>
<div class="modal-header py-2">
<h6 class="modal-title" id="date_range_export_label_<%= export_type %>">
<%= icon("calendar-range") %> <%= t('oroshi.exports.date_range') %>
</h6>
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="modal"
aria-label="<%= t('oroshi.exports.close') %>"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<%= f.label :start_date, t('oroshi.exports.start_date'), class: "form-label small" %>
<%= f.date_field :start_date, value: 1.month.ago.to_date, class: "form-control form-control-sm" %>
</div>
<div class="mb-3">
<%= f.label :end_date, t('oroshi.exports.end_date'), class: "form-label small" %>
<%= f.date_field :end_date, value: Time.zone.today, class: "form-control form-control-sm" %>
</div>
<div class="mb-3">
<%= f.label :format_type, t('oroshi.exports.format'), class: "form-label small" %>
<%= f.select :format_type,
options_for_select([
[t("oroshi.exports.formats.csv"), "csv"],
[t("oroshi.exports.formats.xlsx"), "xlsx"],
[t("oroshi.exports.formats.pdf"), "pdf"],
[t("oroshi.exports.formats.json"), "json"]
], "xlsx"),
{},
{ class: "form-select form-select-sm" } %>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">
<%= t('oroshi.exports.close') %>
</button>
<%= f.submit t('oroshi.exports.download'), class: "btn btn-sm btn-primary" %>
</div>
<% end %>
</div>
</div>
</div>
31 changes: 31 additions & 0 deletions app/views/oroshi/exports/_export_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<%# Reusable export dropdown button for dashboard views %>
<%# locals: (export_type:, date:, additional_params: {}) %>
<div class="dropdown d-inline-block">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
<%= icon("download") %> <%= t('oroshi.exports.button') %>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><h6 class="dropdown-menu-header px-3 py-1 text-muted small"><%= t('oroshi.exports.current_view') %></h6></li>
<% %w[csv xlsx pdf json].each do |fmt| %>
<li>
<%= button_to exports_path,
method: :post,
params: { export_type: export_type, format_type: fmt, date: date }.merge(additional_params),
class: "dropdown-item small",
data: { turbo_prefetch: false } do %>
<%= export_format_icon(fmt) %> <%= t("oroshi.exports.formats.#{fmt}") %>
<% end %>
</li>
<% end %>
<li><hr class="dropdown-divider"></li>
<li>
<a href="#" class="dropdown-item small"
data-bs-toggle="modal" data-bs-target="#date_range_export_modal_<%= export_type %>">
<%= icon("calendar-range") %> <%= t('oroshi.exports.date_range') %>
</a>
</li>
</ul>
</div>

<%= render "oroshi/exports/date_range_export", export_type: export_type %>
7 changes: 5 additions & 2 deletions app/views/oroshi/orders/dashboard/_production.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
<%= production_tab_link('出荷日表', 'shipping_date_production') %>
<%= production_tab_link('製造依頼', 'production_request') %>
<%= production_tab_link('在庫表', 'product_inventory') %>
<%= link_to "依頼反映 #{icon('fast-forward-circle-fill')}".html_safe,
<li class="nav-item ms-auto d-flex align-items-center">
<%= render 'oroshi/exports/export_button', export_type: 'production', date: @date %>
</li>
<%= link_to "依頼反映 #{icon('fast-forward-circle-fill')}".html_safe,
convert_oroshi_production_requests_path(@date),
class: "nav-link ms-auto",
class: "nav-link",
data: {
controller: 'tippy',
tippy_content: '不足している在庫を生産リクエストに変換する',
Expand Down
5 changes: 4 additions & 1 deletion app/views/oroshi/orders/dashboard/_supply_usage.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<%= turbo_frame_tag 'orders_dashboard' do %>
<div class="card mb-2">
<div class="card-header p-1 d-flex gap-2">
<div class="card-header p-1 d-flex gap-2 align-items-center">
供給変種・在庫 表示/非表示
<div class="ms-auto">
<%= render 'oroshi/exports/export_button', export_type: 'inventory', date: @date %>
</div>
</div>
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-center align-items-stretch align-middle gap-2 mb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,18 @@

<%= render 'oroshi/orders/dashboard/shared/filter_buttons/filter_buttons', view:, filter_category:, filter_params: %>
<% end %>

<% export_type = { 'orders' => 'orders', 'templates' => nil, 'shipping' => 'shipping', 'revenue' => 'revenue' }[view] %>
<% if export_type %>
<div class="ms-auto">
<%= render 'oroshi/exports/export_button', export_type: export_type, date: @date,
additional_params: {
buyer_ids: @buyer_ids,
shipping_method_ids: @shipping_method_ids,
order_category_ids: @order_category_ids,
buyer_category_ids: @buyer_category_ids
}.compact %>
</div>
<% end %>
</div>
</div>
9 changes: 6 additions & 3 deletions app/views/oroshi/supply_dates/show/_header.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<h4 class='w-100 flex-fill text-center d-flex justify-content-center align-items-center'>
<%= t('oroshi.supply_dates.show.header_title') %>
</h4>
<div class='w-100 flex-fill d-flex justify-content-center align-items-center gap-2'>
<h4 class="text-center mb-0">
<%= t('oroshi.supply_dates.show.header_title') %>
</h4>
<%= render 'oroshi/exports/export_button', export_type: 'supply', date: @supply_date.date %>
</div>

<%= form_with model: @supply_date,
class: "container-xl d-block d-lg-flex gap-3 mb-3 justify-content-center align-items-center" do |f| %>
Expand Down
26 changes: 26 additions & 0 deletions config/locales/exports.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
en:
oroshi:
exports:
button: "Export"
processing: "Generating export…"
completed: "Export completed"
failed: "Export failed: %{error}"
download: "Download"
start_date: "Start date"
end_date: "End date"
format: "Format"
date_range: "Date range export"
current_view: "Current view"
close: "Close"
formats:
csv: "CSV"
xlsx: "Excel (XLSX)"
pdf: "PDF"
json: "JSON"
types:
orders: "Orders"
revenue: "Revenue & Profit"
production: "Production"
inventory: "Inventory"
supply: "Supply intake"
shipping: "Shipping chart"
26 changes: 26 additions & 0 deletions config/locales/exports.ja.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
ja:
oroshi:
exports:
button: "エクスポート"
processing: "エクスポート処理中…"
completed: "エクスポート完了"
failed: "エクスポートに失敗しました: %{error}"
download: "ダウンロード"
start_date: "開始日"
end_date: "終了日"
format: "形式"
date_range: "期間指定エクスポート"
current_view: "現在の表示"
close: "閉じる"
formats:
csv: "CSV"
xlsx: "Excel (XLSX)"
pdf: "PDF"
json: "JSON"
types:
orders: "注文一覧"
revenue: "売上・利益"
production: "製造・工場"
inventory: "在庫一覧"
supply: "入荷一覧"
shipping: "出荷表"
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
Oroshi::Engine.routes.draw do
root to: "dashboard#index"

# Exports
resources :exports, only: [ :create ]

# Legal pages
get "privacy_policy", to: "legal#privacy_policy", as: :privacy_policy
get "terms_of_service", to: "legal#terms_of_service", as: :terms_of_service
Expand Down
Loading
Loading