+
+ <%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
+ <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
+ Export
+ <% end %>
<% if @template.submitters.to_a.size == 1 %>
- <%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %>
+
+ <%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
+
<% end %>
<% unless @pagy.count.zero? %>
- <%= link_to new_template_submission_path(@template), class: 'btn btn-primary text-base', data: { turbo_frame: 'modal' } do %>
+ <%= link_to new_template_submission_path(@template), class: 'order-1 btn btn-primary text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
- Add Recipients
+ Add Recipients
<% end %>
<% end %>
diff --git a/config/initializers/csv.rb b/config/initializers/csv.rb
new file mode 100644
index 00000000..c278004d
--- /dev/null
+++ b/config/initializers/csv.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require 'csv'
diff --git a/config/routes.rb b/config/routes.rb
index 774d5786..ec81cd86 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,6 +44,7 @@ Rails.application.routes.draw do
resources :console_redirect, only: %i[index]
resources :templates, only: %i[new create edit show destroy] do
resources :submissions, only: %i[new create]
+ resources :submissions_export, only: %i[index new]
end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
diff --git a/lib/submissions/generate_export_files.rb b/lib/submissions/generate_export_files.rb
new file mode 100644
index 00000000..0cd1e6e0
--- /dev/null
+++ b/lib/submissions/generate_export_files.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+module Submissions
+ module GenerateExportFiles
+ UnknownFormat = Class.new(StandardError)
+
+ module_function
+
+ def call(submissions, format: :csv)
+ rows = build_table_rows(submissions)
+
+ if format.to_sym == :csv
+ rows_to_csv(rows)
+ elsif format.to_sym == :xlsx
+ rows_to_xlsx(rows)
+ else
+ raise UnknownFormat
+ end
+ end
+
+ def rows_to_xlsx(rows)
+ workbook = RubyXL::Workbook.new
+ worksheet = workbook[0]
+ worksheet.sheet_name = I18n.l(Time.current.to_date)
+
+ headers = build_headers(rows)
+ headers.each_with_index do |column_name, column_index|
+ worksheet.add_cell(0, column_index, column_name)
+ end
+
+ rows.each.with_index(1) do |row, row_index|
+ extract_columns(row, headers).each_with_index do |value, column_index|
+ worksheet.add_cell(row_index, column_index, value)
+ end
+ end
+
+ workbook.stream.string
+ end
+
+ def rows_to_csv(rows)
+ headers = build_headers(rows)
+
+ CSV.generate do |csv|
+ csv << headers
+
+ rows.each do |row|
+ csv << extract_columns(row, headers)
+ end
+ end
+ end
+
+ def build_headers(rows)
+ rows.reduce(Set.new) { |acc, row| acc + row.pluck(:name) }
+ end
+
+ def extract_columns(row, headers)
+ headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
+ end
+
+ def build_table_rows(submissions)
+ submissions.map do |submission|
+ submission_data = []
+ submitters_count = submission.submitters.size
+
+ submission.submitters.each do |submitter|
+ template_submitters = submission.template_submitters || submission.template.submitters
+ submitter_name = template_submitters.find { |s| s['uuid'] == submitter.uuid }['name']
+
+ submission_data += build_submission_data(submitter, submitter_name, submitters_count)
+
+ submission_data += submitter_formatted_fields(submitter).map do |field|
+ {
+ name: column_name(field[:name], submitter_name, submitters_count),
+ value: field[:value]
+ }
+ end
+
+ next if submitter != submission.submitters.select(&:completed_at?).max_by(&:completed_at)
+
+ submission_data += submitter.documents.map.with_index(1) do |attachment, index|
+ {
+ name: "Document #{index}",
+ value: attachment.url
+ }
+ end
+ end
+
+ submission_data
+ end
+ end
+
+ def build_submission_data(submitter, submitter_name, submitters_count)
+ [
+ {
+ name: column_name('Email', submitter_name, submitters_count),
+ value: submitter.email
+ },
+ {
+ name: column_name('Completed At', submitter_name, submitters_count),
+ value: submitter.completed_at
+ }
+ ]
+ end
+
+ def column_name(name, submitter_name, submitters_count = 1)
+ submitters_count > 1 ? "#{submitter_name} - #{name}" : name
+ end
+
+ def submitter_formatted_fields(submitter)
+ fields = submitter.submission.template_fields || submitter.submission.template.fields
+
+ template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
+
+ attachments_index = submitter.attachments.index_by(&:uuid)
+
+ template_field_counters = Hash.new { 0 }
+ template_fields.map do |template_field|
+ submitter_value = submitter.values.fetch(template_field['uuid'], nil)
+ template_field_type = template_field['type']
+ template_field_counters[template_field_type] += 1
+ template_field_name = template_field['name'].presence
+ template_field_name ||= "#{template_field_type.titleize} Field #{template_field_counters[template_field_type]}"
+
+ value =
+ if template_field_type.in?(%w[image signature])
+ attachments_index[submitter_value]&.url
+ elsif template_field_type == 'file'
+ Array.wrap(submitter_value).compact_blank.filter_map { |e| attachments_index[e]&.url }
+ else
+ submitter_value
+ end
+
+ { name: template_field_name, uuid: template_field['uuid'], value: }
+ end
+ end
+ end
+end