Skip to content
Merged
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
30 changes: 30 additions & 0 deletions app/controllers/import_reports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class ImportReportsController < ApplicationController
include Pagy::Backend

before_action :set_import_report, only: [ :show ]

def index
@pagy, @import_reports = pagy(scope.includes(:import_errors))

# Optional filtering by status
@import_reports = @import_reports.where(status: params[:status]) if params[:status].present?

# Collect unique statuses and import types for filter dropdowns
@available_statuses = ImportReport.statuses.keys
@available_import_types = scope.distinct.pluck(:import_type).compact.sort
end

def show
@import_errors = @import_report.import_errors
end

private

def set_import_report
@import_report = ImportReport.includes(:import_errors).find(params[:id])
end

def scope
@scope ||= ImportReport.all
end
end
32 changes: 32 additions & 0 deletions app/models/import_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# == Schema Information
#
# Table name: import_errors
#
# id :bigint not null, primary key
# error_message :text
# error_type :string not null
# file_name :string
# metadata :json
# created_at :datetime not null
# updated_at :datetime not null
# import_report_id :bigint not null
# topic_id :integer
#
# Indexes
#
# index_import_errors_on_error_type (error_type)
# index_import_errors_on_file_name (file_name)
# index_import_errors_on_import_report_id (import_report_id)
#
# Foreign Keys
#
# fk_rails_... (import_report_id => import_reports.id)
#
class ImportError < ApplicationRecord
belongs_to :import_report

validates :error_type, presence: true

scope :by_type, ->(type) { where(error_type: type) }
scope :with_files, -> { where.not(file_name: nil) }
end
29 changes: 29 additions & 0 deletions app/models/import_report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# == Schema Information
#
# Table name: import_reports
#
# id :bigint not null, primary key
# completed_at :datetime
# error_details :json
# import_type :string not null
# started_at :datetime
# status :string default("pending")
# summary_stats :json
# unmatched_files :json
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_import_reports_on_import_type (import_type)
#
class ImportReport < ApplicationRecord
has_many :import_errors, dependent: :destroy

validates :import_type, presence: true

enum :status, { pending: "pending", planned: "planned", completed: "completed", failed: "failed" }

scope :recent, -> { order(created_at: :desc) }
scope :by_type, ->(type) { where(import_type: type) }
end
79 changes: 79 additions & 0 deletions app/views/import_reports/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<% content_for :title, "Import Reports" %>

<section class="section">
<div class="row" id="table-striped">
<div class="col-12 cold-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="card-title">Import Reports</h2>
</div>
<div class="card-content">
<div class="card-body">
<p class="card-text">View and monitor import operations and their status.</p>

<!-- Filters -->
<div class="row mb-3">
<div class="col-md-4">
<%= form_with url: import_reports_path, method: :get, local: true, class: "d-flex gap-2" do |form| %>
<%= form.select :status, options_for_select([["All Statuses", ""]] + @available_statuses.map { |status| [status.humanize, status] }, params[:status]), {}, { class: "form-select", onchange: "this.form.submit();" } %>
<% end %>
</div>
</div>

<div class="table-responsive">
<table class="table table-lg table-striped mb-0">
<thead>
<tr>
<th>ID</th>
<th>Import Type</th>
<th>Status</th>
<th>Started At</th>
<th>Completed At</th>
<th>Errors</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<% @import_reports.each do |import_report| %>
<tr>
<td class="text-bold-500"><%= import_report.id %></td>
<td class="text-bold-500"><%= import_report.import_type.humanize %></td>
<td>
<span class="badge bg-<%= import_report.status == "completed" ? "success" : "info" %>">
<%= import_report.status.humanize %>
</span>
</td>
<td class="text-bold-500">
<%= import_report.started_at&.strftime("%m/%d/%Y %I:%M %p") || "Not started" %>
</td>
<td class="text-bold-500">
<%= import_report.completed_at&.strftime("%m/%d/%Y %I:%M %p") || "Not completed" %>
</td>
<td class="text-bold-500">
<% if import_report.import_errors.any? %>
<span class="badge bg-danger"><%= import_report.import_errors.count %></span>
<% else %>
<span class="badge bg-success">0</span>
<% end %>
</td>
<td class="text-end">
<%= link_to import_report_path(import_report), class: "btn btn-secondary btn-sm" do %>
<i class="bi bi-eye"></i> View
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

<!-- Pagination -->
<div class="d-flex justify-content-center mt-3">
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
132 changes: 132 additions & 0 deletions app/views/import_reports/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<% content_for :title, "Import Report ##{@import_report.id}" %>

<div class="section">
<h3 class="mb-4">Import Report: #<%= @import_report.id %></h3>
<div class="card mb-6">
<div class="card-header">
<div class="card-title">
<h3><%= @import_report.import_type.humanize %> Import</h3>
</div>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-3"><strong>Status:</strong></div>
<div class="col-md-9">
<span class="badge bg-alert %>">
<%= @import_report.status %>
</span>
</div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong>Import Type:</strong></div>
<div class="col-md-9"><%= @import_report.import_type.humanize %></div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong>Started At:</strong></div>
<div class="col-md-9">
<%= @import_report.started_at&.strftime('%m/%d/%Y %I:%M %p') || "Not started" %>
</div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong>Completed At:</strong></div>
<div class="col-md-9">
<%= @import_report.completed_at&.strftime('%m/%d/%Y %I:%M %p') || "Not completed" %>
</div>
</div>
<% if @import_report.summary_stats.present? %>
<div class="row mb-2">
<div class="col-md-3"><strong>Summary Stats:</strong></div>
<div class="col-md-9">
<pre class="bg-light p-2 rounded"><%= JSON.pretty_generate(@import_report.summary_stats) %></pre>
</div>
</div>
<% end %>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-3"><strong>Created:</strong></div>
<div class="col-md-9"><%= @import_report.created_at.strftime('%m/%d/%Y %I:%M %p') %></div>
</div>
</div>
</div>
</div>

<div class="section">
<div class="col-12">
<h3 class="mb-4">
Import Errors
<span class="badge bg-<%= @import_errors.any? ? 'danger' : 'success' %>">
<%= @import_errors.count %>
</span>
</h3>
</div>

<% if @import_errors.any? %>
<div class="card">
<div class="card-content">
<div class="card-body">
<div class="table-responsive">
<table class="table table-lg table-striped mb-0">
<thead>
<tr>
<th>Error Type</th>
<th>File Name</th>
<th>Error Message</th>
<th>Topic ID</th>
</tr>
</thead>
<tbody>
<% @import_errors.each do |error| %>
<tr>
<td>
<span class="badge bg-danger"><%= error.error_type %></span>
</td>
<td class="text-bold-500">
<div class="text-truncate"title="<%= error.file_name %>">
<%= error.file_name || "N/A" %>
</div>
</td>
<td class="text-bold-500">
<% if error.error_message.present? %>
<div class="text-truncate" style="max-width: 300px;" title="<%= error.error_message %>">
<%= error.error_message %>
</div>
<% else %>
N/A
<% end %>
</td>
<td class="text-bold-500">
<%= error.topic_id || "N/A" %>
</td>
</tr>
<% if error.metadata.present? %>
<tr>
<td colspan="5" class="bg-light">
<small><strong>Metadata:</strong></small>
<pre class="small mb-0"><%= JSON.pretty_generate(error.metadata) %></pre>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
<% else %>
<div class="card">
<div class="card-body text-center">
<div class="text-success">
<i class="bi bi-check-circle-fill" style="font-size: 3rem;"></i>
</div>
<h4 class="mt-3">No Errors Found</h4>
<p class="text-muted">This import completed successfully without any errors.</p>
</div>
</div>
<% end %>
</div>

<div class="mt-4">
<%= link_to "Back to Import Reports", import_reports_path, class: "btn btn-secondary" %>
</div>
7 changes: 7 additions & 0 deletions app/views/layouts/_sidebar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@
<span>Users</span>
<% end %>
</li>

<li class="sidebar-item">
<%= link_to import_reports_path, class: 'sidebar-link' do %>
<i class="bi bi-circle"></i>
<span>Import Reports</span>
<% end %>
</li>
<% end %>
</ul>
</div>
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get :tags
end
end
resources :import_reports, only: %i[index show]
resource :settings, only: [] do
put :provider, on: :collection
end
Expand Down
17 changes: 17 additions & 0 deletions db/migrate/20250625101008_create_import_reports.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateImportReports < ActiveRecord::Migration[8.0]
def change
create_table :import_reports do |t|
t.string :import_type, null: false
t.datetime :started_at
t.datetime :completed_at
t.json :summary_stats
t.json :unmatched_files
t.json :error_details
t.string :status, default: 'pending'

t.timestamps
end

add_index :import_reports, :import_type
end
end
17 changes: 17 additions & 0 deletions db/migrate/20250625101109_create_import_errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateImportErrors < ActiveRecord::Migration[7.0]
def change
create_table :import_errors do |t|
t.references :import_report, null: false, foreign_key: true
t.string :error_type, null: false
t.string :file_name
t.integer :topic_id
t.text :error_message
t.json :metadata

t.timestamps
end

add_index :import_errors, :error_type
add_index :import_errors, :file_name
end
end
Loading