Skip to content

Commit 4df5137

Browse files
authored
Import report (#221)
2 parents 5137ec3 + 5216c26 commit 4df5137

File tree

11 files changed

+477
-20
lines changed

11 files changed

+477
-20
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class ImportReportsController < ApplicationController
2+
include Pagy::Backend
3+
4+
before_action :set_import_report, only: [ :show ]
5+
6+
def index
7+
@pagy, @import_reports = pagy(scope.includes(:import_errors))
8+
9+
# Optional filtering by status
10+
@import_reports = @import_reports.where(status: params[:status]) if params[:status].present?
11+
12+
# Collect unique statuses and import types for filter dropdowns
13+
@available_statuses = ImportReport.statuses.keys
14+
@available_import_types = scope.distinct.pluck(:import_type).compact.sort
15+
end
16+
17+
def show
18+
@import_errors = @import_report.import_errors
19+
end
20+
21+
private
22+
23+
def set_import_report
24+
@import_report = ImportReport.includes(:import_errors).find(params[:id])
25+
end
26+
27+
def scope
28+
@scope ||= ImportReport.all
29+
end
30+
end

app/models/import_error.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# == Schema Information
2+
#
3+
# Table name: import_errors
4+
#
5+
# id :bigint not null, primary key
6+
# error_message :text
7+
# error_type :string not null
8+
# file_name :string
9+
# metadata :json
10+
# created_at :datetime not null
11+
# updated_at :datetime not null
12+
# import_report_id :bigint not null
13+
# topic_id :integer
14+
#
15+
# Indexes
16+
#
17+
# index_import_errors_on_error_type (error_type)
18+
# index_import_errors_on_file_name (file_name)
19+
# index_import_errors_on_import_report_id (import_report_id)
20+
#
21+
# Foreign Keys
22+
#
23+
# fk_rails_... (import_report_id => import_reports.id)
24+
#
25+
class ImportError < ApplicationRecord
26+
belongs_to :import_report
27+
28+
validates :error_type, presence: true
29+
30+
scope :by_type, ->(type) { where(error_type: type) }
31+
scope :with_files, -> { where.not(file_name: nil) }
32+
end

app/models/import_report.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# == Schema Information
2+
#
3+
# Table name: import_reports
4+
#
5+
# id :bigint not null, primary key
6+
# completed_at :datetime
7+
# error_details :json
8+
# import_type :string not null
9+
# started_at :datetime
10+
# status :string default("pending")
11+
# summary_stats :json
12+
# unmatched_files :json
13+
# created_at :datetime not null
14+
# updated_at :datetime not null
15+
#
16+
# Indexes
17+
#
18+
# index_import_reports_on_import_type (import_type)
19+
#
20+
class ImportReport < ApplicationRecord
21+
has_many :import_errors, dependent: :destroy
22+
23+
validates :import_type, presence: true
24+
25+
enum :status, { pending: "pending", planned: "planned", completed: "completed", failed: "failed" }
26+
27+
scope :recent, -> { order(created_at: :desc) }
28+
scope :by_type, ->(type) { where(import_type: type) }
29+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<% content_for :title, "Import Reports" %>
2+
3+
<section class="section">
4+
<div class="row" id="table-striped">
5+
<div class="col-12 cold-md-12">
6+
<div class="card">
7+
<div class="card-header d-flex justify-content-between align-items-center">
8+
<h2 class="card-title">Import Reports</h2>
9+
</div>
10+
<div class="card-content">
11+
<div class="card-body">
12+
<p class="card-text">View and monitor import operations and their status.</p>
13+
14+
<!-- Filters -->
15+
<div class="row mb-3">
16+
<div class="col-md-4">
17+
<%= form_with url: import_reports_path, method: :get, local: true, class: "d-flex gap-2" do |form| %>
18+
<%= form.select :status, options_for_select([["All Statuses", ""]] + @available_statuses.map { |status| [status.humanize, status] }, params[:status]), {}, { class: "form-select", onchange: "this.form.submit();" } %>
19+
<% end %>
20+
</div>
21+
</div>
22+
23+
<div class="table-responsive">
24+
<table class="table table-lg table-striped mb-0">
25+
<thead>
26+
<tr>
27+
<th>ID</th>
28+
<th>Import Type</th>
29+
<th>Status</th>
30+
<th>Started At</th>
31+
<th>Completed At</th>
32+
<th>Errors</th>
33+
<th class="text-end">Actions</th>
34+
</tr>
35+
</thead>
36+
<tbody>
37+
<% @import_reports.each do |import_report| %>
38+
<tr>
39+
<td class="text-bold-500"><%= import_report.id %></td>
40+
<td class="text-bold-500"><%= import_report.import_type.humanize %></td>
41+
<td>
42+
<span class="badge bg-<%= import_report.status == "completed" ? "success" : "info" %>">
43+
<%= import_report.status.humanize %>
44+
</span>
45+
</td>
46+
<td class="text-bold-500">
47+
<%= import_report.started_at&.strftime("%m/%d/%Y %I:%M %p") || "Not started" %>
48+
</td>
49+
<td class="text-bold-500">
50+
<%= import_report.completed_at&.strftime("%m/%d/%Y %I:%M %p") || "Not completed" %>
51+
</td>
52+
<td class="text-bold-500">
53+
<% if import_report.import_errors.any? %>
54+
<span class="badge bg-danger"><%= import_report.import_errors.count %></span>
55+
<% else %>
56+
<span class="badge bg-success">0</span>
57+
<% end %>
58+
</td>
59+
<td class="text-end">
60+
<%= link_to import_report_path(import_report), class: "btn btn-secondary btn-sm" do %>
61+
<i class="bi bi-eye"></i> View
62+
<% end %>
63+
</td>
64+
</tr>
65+
<% end %>
66+
</tbody>
67+
</table>
68+
</div>
69+
70+
<!-- Pagination -->
71+
<div class="d-flex justify-content-center mt-3">
72+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
</section>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<% content_for :title, "Import Report ##{@import_report.id}" %>
2+
3+
<div class="section">
4+
<h3 class="mb-4">Import Report: #<%= @import_report.id %></h3>
5+
<div class="card mb-6">
6+
<div class="card-header">
7+
<div class="card-title">
8+
<h3><%= @import_report.import_type.humanize %> Import</h3>
9+
</div>
10+
</div>
11+
<div class="card-body">
12+
<div class="row mb-2">
13+
<div class="col-md-3"><strong>Status:</strong></div>
14+
<div class="col-md-9">
15+
<span class="badge bg-alert %>">
16+
<%= @import_report.status %>
17+
</span>
18+
</div>
19+
</div>
20+
<div class="row mb-2">
21+
<div class="col-md-3"><strong>Import Type:</strong></div>
22+
<div class="col-md-9"><%= @import_report.import_type.humanize %></div>
23+
</div>
24+
<div class="row mb-2">
25+
<div class="col-md-3"><strong>Started At:</strong></div>
26+
<div class="col-md-9">
27+
<%= @import_report.started_at&.strftime('%m/%d/%Y %I:%M %p') || "Not started" %>
28+
</div>
29+
</div>
30+
<div class="row mb-2">
31+
<div class="col-md-3"><strong>Completed At:</strong></div>
32+
<div class="col-md-9">
33+
<%= @import_report.completed_at&.strftime('%m/%d/%Y %I:%M %p') || "Not completed" %>
34+
</div>
35+
</div>
36+
<% if @import_report.summary_stats.present? %>
37+
<div class="row mb-2">
38+
<div class="col-md-3"><strong>Summary Stats:</strong></div>
39+
<div class="col-md-9">
40+
<pre class="bg-light p-2 rounded"><%= JSON.pretty_generate(@import_report.summary_stats) %></pre>
41+
</div>
42+
</div>
43+
<% end %>
44+
</div>
45+
<div class="card-footer">
46+
<div class="row">
47+
<div class="col-md-3"><strong>Created:</strong></div>
48+
<div class="col-md-9"><%= @import_report.created_at.strftime('%m/%d/%Y %I:%M %p') %></div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
54+
<div class="section">
55+
<div class="col-12">
56+
<h3 class="mb-4">
57+
Import Errors
58+
<span class="badge bg-<%= @import_errors.any? ? 'danger' : 'success' %>">
59+
<%= @import_errors.count %>
60+
</span>
61+
</h3>
62+
</div>
63+
64+
<% if @import_errors.any? %>
65+
<div class="card">
66+
<div class="card-content">
67+
<div class="card-body">
68+
<div class="table-responsive">
69+
<table class="table table-lg table-striped mb-0">
70+
<thead>
71+
<tr>
72+
<th>Error Type</th>
73+
<th>File Name</th>
74+
<th>Error Message</th>
75+
<th>Topic ID</th>
76+
</tr>
77+
</thead>
78+
<tbody>
79+
<% @import_errors.each do |error| %>
80+
<tr>
81+
<td>
82+
<span class="badge bg-danger"><%= error.error_type %></span>
83+
</td>
84+
<td class="text-bold-500">
85+
<div class="text-truncate"title="<%= error.file_name %>">
86+
<%= error.file_name || "N/A" %>
87+
</div>
88+
</td>
89+
<td class="text-bold-500">
90+
<% if error.error_message.present? %>
91+
<div class="text-truncate" style="max-width: 300px;" title="<%= error.error_message %>">
92+
<%= error.error_message %>
93+
</div>
94+
<% else %>
95+
N/A
96+
<% end %>
97+
</td>
98+
<td class="text-bold-500">
99+
<%= error.topic_id || "N/A" %>
100+
</td>
101+
</tr>
102+
<% if error.metadata.present? %>
103+
<tr>
104+
<td colspan="5" class="bg-light">
105+
<small><strong>Metadata:</strong></small>
106+
<pre class="small mb-0"><%= JSON.pretty_generate(error.metadata) %></pre>
107+
</td>
108+
</tr>
109+
<% end %>
110+
<% end %>
111+
</tbody>
112+
</table>
113+
</div>
114+
</div>
115+
</div>
116+
</div>
117+
<% else %>
118+
<div class="card">
119+
<div class="card-body text-center">
120+
<div class="text-success">
121+
<i class="bi bi-check-circle-fill" style="font-size: 3rem;"></i>
122+
</div>
123+
<h4 class="mt-3">No Errors Found</h4>
124+
<p class="text-muted">This import completed successfully without any errors.</p>
125+
</div>
126+
</div>
127+
<% end %>
128+
</div>
129+
130+
<div class="mt-4">
131+
<%= link_to "Back to Import Reports", import_reports_path, class: "btn btn-secondary" %>
132+
</div>

app/views/layouts/_sidebar.html.erb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@
8888
<span>Users</span>
8989
<% end %>
9090
</li>
91+
92+
<li class="sidebar-item">
93+
<%= link_to import_reports_path, class: 'sidebar-link' do %>
94+
<i class="bi bi-circle"></i>
95+
<span>Import Reports</span>
96+
<% end %>
97+
</li>
9198
<% end %>
9299
</ul>
93100
</div>

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
get :tags
1818
end
1919
end
20+
resources :import_reports, only: %i[index show]
2021
resource :settings, only: [] do
2122
put :provider, on: :collection
2223
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class CreateImportReports < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :import_reports do |t|
4+
t.string :import_type, null: false
5+
t.datetime :started_at
6+
t.datetime :completed_at
7+
t.json :summary_stats
8+
t.json :unmatched_files
9+
t.json :error_details
10+
t.string :status, default: 'pending'
11+
12+
t.timestamps
13+
end
14+
15+
add_index :import_reports, :import_type
16+
end
17+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class CreateImportErrors < ActiveRecord::Migration[7.0]
2+
def change
3+
create_table :import_errors do |t|
4+
t.references :import_report, null: false, foreign_key: true
5+
t.string :error_type, null: false
6+
t.string :file_name
7+
t.integer :topic_id
8+
t.text :error_message
9+
t.json :metadata
10+
11+
t.timestamps
12+
end
13+
14+
add_index :import_errors, :error_type
15+
add_index :import_errors, :file_name
16+
end
17+
end

0 commit comments

Comments
 (0)