+sub action_report {
+  my ($self, %params) = @_;
+
+  my $report_id   = $params{report_id} || $::form->{id};
+  $self->{report} = SL::DB::Manager::CsvImportReport->find_by(id => $report_id);
+
+  if (!$self->{report}) {
+    $::form->error(t8('No report with id #1', $report_id));
+  }
+
+  my $show_info_err = ($self->{report}->profile->get('full_preview', 0) == 1);
+  my $show_first_20 = ($self->{report}->profile->get('full_preview', 0) == 2);
+
+  my $num_rows = 0;
+  if ($show_first_20) {
+    $num_rows  = min($self->{report}->numrows, 20);
+  } elsif ($show_info_err) {
+    # count each status row only once
+    $num_rows  = SL::DB::Manager::CsvImportReportStatus->get_all_count(query    => [csv_import_report_id => $report_id],
+                                                                       select   => ['row'],
+                                                                       distinct => 1,);
+  } else {
+    # show all
+    $num_rows  = $self->{report}->numrows;
+  }
+
+  # manual paginating, yuck
+  my $page                   = $::form->{page} || 1;
+  my $pages                  = {};
+  $pages->{per_page}         = $::form->{per_page} || 20;
+  $pages->{max}              = SL::DB::Helper::Paginated::ceil($num_rows, $pages->{per_page}) || 1;
+  $pages->{page}             = $page < 1             ? 1
+                             : $page > $pages->{max} ? $pages->{max}
+                             : $                       page;
+  $pages->{common}           = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+
+  $self->{report_numheaders} = $self->{report}->numheaders;
+  my $first_row_header       = 0;
+  my $last_row_header        = $self->{report_numheaders} - 1;
+  my $first_row_data         = $pages->{per_page} * ($pages->{page}-1) + $self->{report_numheaders};
+  my $last_row_data          = min($pages->{per_page} * $pages->{page}, $num_rows) + $self->{report_numheaders} - 1;
+
+
+  $self->{display_rows} = [];
+  if ($show_info_err) {
+    my $limit    = $last_row_data  - $first_row_data + 1;
+    my $offset   = $first_row_data - $self->{report_numheaders};
+    my @err_rows = map { $_->row } @{SL::DB::Manager::CsvImportReportStatus->get_all(query    => [csv_import_report_id => $report_id],
+                                                                                     distinct => 1,
+                                                                                     select   => ['row'],
+                                                                                     limit    => $limit,
+                                                                                     offset   => $offset,
+                                                                                     sort_by  => 'row')};
+    $self->{display_rows} = [ $first_row_header .. $last_row_header,
+                              @err_rows ];
+
+  } else {
+
+    $self->{display_rows} = [ $first_row_header .. $last_row_header,
+                              $first_row_data   .. $last_row_data ];
+  }
+
+  my @query = (
+    row                  => $self->{display_rows},
+    csv_import_report_id => $report_id,
+  );
+
+  my $rows               = SL::DB::Manager::CsvImportReportRow   ->get_all(query => \@query, sort_by => 'row');
+  my $status             = SL::DB::Manager::CsvImportReportStatus->get_all(query => \@query, sort_by => 'row');
+  $self->{num_errors}    = SL::DB::Manager::CsvImportReportStatus->get_all_count(query => [csv_import_report_id => $report_id, type => 'errors']);
+
+  $self->{report_rows}   = $self->{report}->folded_rows(rows => $rows);
+  $self->{report_status} = $self->{report}->folded_status(status => $status);
+  $self->{pages}         = $pages;
+  $self->{base_url}      = $self->url_for(action => 'report', id => $report_id, no_layout => $params{no_layout} || $::form->{no_layout} );
+
+  $self->render('csv_import/report', { layout => !($params{no_layout} || $::form->{no_layout}) });
+}
+
+sub action_add_empty_mapping_line {
+  my ($self) = @_;
+
+  $self->profile_from_form;
+  $self->setup_help;
+
+  $self->js
+    ->append('#csv_import_mappings', $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }))
+    ->hide('#mapping_empty')
+    ->render;
+}
+
+sub action_add_mapping_from_upload {
+  my ($self) = @_;
+
+  if ($::form->{tmp_profile_id}) {
+    $self->profile_from_form(SL::DB::CsvImportProfile->new(id => $::form->{tmp_profile_id})->load);
+  } else {
+    $self->profile_from_form;
+  }
+  $self->setup_help;
+
+  my $file_name;
+  if ($self->profile->get('file_name')) {
+    $file_name = $self->profile->get('file_name');
+  } else {
+    $self->js
+      ->flash('error', t8('No file has been uploaded yet.'))
+      ->render;
+    return;
+  }
+
+  my $file = SL::SessionFile->new($file_name, mode => '<', encoding => $self->profile->get('charset'));
+  if (!$file->fh) {
+    $self->js
+      ->flash('error', t8('No file has been uploaded yet.'))
+      ->render;
+    return;
+  }
+
+  my $csv = SL::Helper::Csv->new(
+    file => $file->file_name,
+    map { $_ => $self->profile->get($_) } qw(sep_char escape_char quote_char),
+  );
+
+  $csv->_open_file;
+  my $header = $csv->check_header;
+
+  for my $field (@$header) {
+    next if $self->mappings_for_profile->{$field};
+    $self->js->append(
+      '#csv_import_mappings',
+      $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }, item => { from => $field }),
+    );
+  }
+
+  $self->js
+    ->hide('#mapping_empty')
+    ->render;
+}
+
+