claude-agent-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
# Skill: Imports & Exports
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implementar funcionalidades de importación y exportación de datos en formatos CSV, Excel y JSON para aplicaciones Rails.
|
|
6
|
+
|
|
7
|
+
## CSV Export
|
|
8
|
+
|
|
9
|
+
### Export simple
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# app/controllers/users_controller.rb
|
|
13
|
+
class UsersController < ApplicationController
|
|
14
|
+
def index
|
|
15
|
+
@users = User.all
|
|
16
|
+
|
|
17
|
+
respond_to do |format|
|
|
18
|
+
format.html
|
|
19
|
+
format.csv { send_data @users.to_csv, filename: "users-#{Date.current}.csv" }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# app/models/user.rb
|
|
25
|
+
class User < ApplicationRecord
|
|
26
|
+
def self.to_csv
|
|
27
|
+
attributes = %w[id name email created_at]
|
|
28
|
+
|
|
29
|
+
CSV.generate(headers: true) do |csv|
|
|
30
|
+
csv << attributes
|
|
31
|
+
|
|
32
|
+
all.find_each do |user|
|
|
33
|
+
csv << attributes.map { |attr| user.send(attr) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Export con headers personalizados
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# app/models/concerns/csv_exportable.rb
|
|
44
|
+
module CsvExportable
|
|
45
|
+
extend ActiveSupport::Concern
|
|
46
|
+
|
|
47
|
+
class_methods do
|
|
48
|
+
def to_csv(columns: nil, headers: nil)
|
|
49
|
+
columns ||= column_names
|
|
50
|
+
headers ||= columns.map(&:humanize)
|
|
51
|
+
|
|
52
|
+
CSV.generate(headers: true) do |csv|
|
|
53
|
+
csv << headers
|
|
54
|
+
|
|
55
|
+
find_each do |record|
|
|
56
|
+
csv << columns.map { |col| format_csv_value(record, col) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def format_csv_value(record, column)
|
|
64
|
+
value = record.send(column)
|
|
65
|
+
|
|
66
|
+
case value
|
|
67
|
+
when Time, DateTime
|
|
68
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
|
69
|
+
when Date
|
|
70
|
+
value.strftime("%Y-%m-%d")
|
|
71
|
+
when true
|
|
72
|
+
"Sí"
|
|
73
|
+
when false
|
|
74
|
+
"No"
|
|
75
|
+
when nil
|
|
76
|
+
""
|
|
77
|
+
else
|
|
78
|
+
value.to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# app/models/order.rb
|
|
85
|
+
class Order < ApplicationRecord
|
|
86
|
+
include CsvExportable
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Uso
|
|
90
|
+
Order.where(status: "completed").to_csv(
|
|
91
|
+
columns: %w[id user_name total status created_at],
|
|
92
|
+
headers: ["ID", "Cliente", "Total", "Estado", "Fecha"]
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Export streaming (archivos grandes)
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# app/controllers/exports_controller.rb
|
|
100
|
+
class ExportsController < ApplicationController
|
|
101
|
+
include ActionController::Live
|
|
102
|
+
|
|
103
|
+
def orders
|
|
104
|
+
response.headers["Content-Type"] = "text/csv"
|
|
105
|
+
response.headers["Content-Disposition"] = "attachment; filename=orders-#{Date.current}.csv"
|
|
106
|
+
|
|
107
|
+
response.stream.write CSV.generate_line(["ID", "Cliente", "Total", "Estado"])
|
|
108
|
+
|
|
109
|
+
Order.find_each do |order|
|
|
110
|
+
response.stream.write CSV.generate_line([
|
|
111
|
+
order.id,
|
|
112
|
+
order.user.name,
|
|
113
|
+
order.total,
|
|
114
|
+
order.status
|
|
115
|
+
])
|
|
116
|
+
end
|
|
117
|
+
ensure
|
|
118
|
+
response.stream.close
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## CSV Import
|
|
124
|
+
|
|
125
|
+
### Import básico
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# app/services/csv_importer.rb
|
|
129
|
+
class CsvImporter
|
|
130
|
+
def initialize(file, model_class)
|
|
131
|
+
@file = file
|
|
132
|
+
@model_class = model_class
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def import
|
|
136
|
+
results = { success: 0, errors: [] }
|
|
137
|
+
|
|
138
|
+
CSV.foreach(@file.path, headers: true) do |row|
|
|
139
|
+
record = @model_class.new(row.to_h)
|
|
140
|
+
|
|
141
|
+
if record.save
|
|
142
|
+
results[:success] += 1
|
|
143
|
+
else
|
|
144
|
+
results[:errors] << {
|
|
145
|
+
row: $INPUT_LINE_NUMBER,
|
|
146
|
+
errors: record.errors.full_messages
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
results
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Uso en controller
|
|
156
|
+
class ImportsController < ApplicationController
|
|
157
|
+
def create
|
|
158
|
+
if params[:file].blank?
|
|
159
|
+
redirect_to imports_path, alert: "Selecciona un archivo"
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
result = CsvImporter.new(params[:file], User).import
|
|
164
|
+
|
|
165
|
+
if result[:errors].empty?
|
|
166
|
+
redirect_to users_path, notice: "#{result[:success]} usuarios importados"
|
|
167
|
+
else
|
|
168
|
+
flash.now[:alert] = "Errores en la importación"
|
|
169
|
+
@errors = result[:errors]
|
|
170
|
+
render :new
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Import con validación previa
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# app/services/csv_import_service.rb
|
|
180
|
+
class CsvImportService
|
|
181
|
+
Result = Struct.new(:success?, :imported, :errors, keyword_init: true)
|
|
182
|
+
|
|
183
|
+
def initialize(file, model_class, column_mapping: {})
|
|
184
|
+
@file = file
|
|
185
|
+
@model_class = model_class
|
|
186
|
+
@column_mapping = column_mapping
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def preview(limit: 5)
|
|
190
|
+
rows = []
|
|
191
|
+
CSV.foreach(@file.path, headers: true).with_index do |row, index|
|
|
192
|
+
break if index >= limit
|
|
193
|
+
rows << transform_row(row)
|
|
194
|
+
end
|
|
195
|
+
rows
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def validate
|
|
199
|
+
errors = []
|
|
200
|
+
line = 2 # Línea 1 son headers
|
|
201
|
+
|
|
202
|
+
CSV.foreach(@file.path, headers: true) do |row|
|
|
203
|
+
record = @model_class.new(transform_row(row))
|
|
204
|
+
unless record.valid?
|
|
205
|
+
errors << { line: line, errors: record.errors.full_messages }
|
|
206
|
+
end
|
|
207
|
+
line += 1
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
errors
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def import
|
|
214
|
+
imported = 0
|
|
215
|
+
errors = []
|
|
216
|
+
|
|
217
|
+
ActiveRecord::Base.transaction do
|
|
218
|
+
CSV.foreach(@file.path, headers: true).with_index(2) do |row, line|
|
|
219
|
+
record = @model_class.new(transform_row(row))
|
|
220
|
+
|
|
221
|
+
if record.save
|
|
222
|
+
imported += 1
|
|
223
|
+
else
|
|
224
|
+
errors << { line: line, errors: record.errors.full_messages }
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Rollback si hay muchos errores
|
|
229
|
+
if errors.size > imported * 0.1 # Más del 10% de errores
|
|
230
|
+
raise ActiveRecord::Rollback
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
Result.new(success?: errors.empty?, imported: imported, errors: errors)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def transform_row(row)
|
|
240
|
+
if @column_mapping.present?
|
|
241
|
+
@column_mapping.transform_values { |csv_col| row[csv_col] }
|
|
242
|
+
else
|
|
243
|
+
row.to_h
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Uso
|
|
249
|
+
service = CsvImportService.new(
|
|
250
|
+
params[:file],
|
|
251
|
+
Product,
|
|
252
|
+
column_mapping: {
|
|
253
|
+
name: "Nombre",
|
|
254
|
+
price: "Precio",
|
|
255
|
+
sku: "Código"
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Preview
|
|
260
|
+
@preview = service.preview(limit: 10)
|
|
261
|
+
|
|
262
|
+
# Validar antes de importar
|
|
263
|
+
@validation_errors = service.validate
|
|
264
|
+
return render :new if @validation_errors.any?
|
|
265
|
+
|
|
266
|
+
# Importar
|
|
267
|
+
result = service.import
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Import con job en background
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# app/jobs/import_job.rb
|
|
274
|
+
class ImportJob < ApplicationJob
|
|
275
|
+
queue_as :imports
|
|
276
|
+
|
|
277
|
+
def perform(import_id)
|
|
278
|
+
import = Import.find(import_id)
|
|
279
|
+
import.update!(status: "processing")
|
|
280
|
+
|
|
281
|
+
file_path = ActiveStorage::Blob.service.path_for(import.file.key)
|
|
282
|
+
result = CsvImportService.new(
|
|
283
|
+
File.open(file_path),
|
|
284
|
+
import.model_class.constantize
|
|
285
|
+
).import
|
|
286
|
+
|
|
287
|
+
import.update!(
|
|
288
|
+
status: result.success? ? "completed" : "completed_with_errors",
|
|
289
|
+
imported_count: result.imported,
|
|
290
|
+
error_details: result.errors
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Notificar al usuario
|
|
294
|
+
ImportMailer.completed(import).deliver_later
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
import.update!(status: "failed", error_details: [{ error: e.message }])
|
|
297
|
+
raise
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# app/models/import.rb
|
|
302
|
+
class Import < ApplicationRecord
|
|
303
|
+
belongs_to :user
|
|
304
|
+
has_one_attached :file
|
|
305
|
+
|
|
306
|
+
enum :status, {
|
|
307
|
+
pending: "pending",
|
|
308
|
+
processing: "processing",
|
|
309
|
+
completed: "completed",
|
|
310
|
+
completed_with_errors: "completed_with_errors",
|
|
311
|
+
failed: "failed"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
validates :file, presence: true
|
|
315
|
+
validate :file_format
|
|
316
|
+
|
|
317
|
+
after_create_commit :process_later
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
def file_format
|
|
322
|
+
return unless file.attached?
|
|
323
|
+
unless file.content_type.in?(%w[text/csv application/vnd.ms-excel])
|
|
324
|
+
errors.add(:file, "debe ser un archivo CSV")
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def process_later
|
|
329
|
+
ImportJob.perform_later(id)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Excel Export (XLSX)
|
|
335
|
+
|
|
336
|
+
### Setup
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Gemfile
|
|
340
|
+
gem "caxlsx"
|
|
341
|
+
gem "caxlsx_rails"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Export básico
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# app/views/orders/index.xlsx.axlsx
|
|
348
|
+
wb = xlsx_package.workbook
|
|
349
|
+
|
|
350
|
+
wb.add_worksheet(name: "Pedidos") do |sheet|
|
|
351
|
+
# Estilos
|
|
352
|
+
header_style = sheet.styles.add_style(
|
|
353
|
+
bg_color: "4472C4",
|
|
354
|
+
fg_color: "FFFFFF",
|
|
355
|
+
b: true,
|
|
356
|
+
alignment: { horizontal: :center }
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
money_style = sheet.styles.add_style(
|
|
360
|
+
num_fmt: "€#,##0.00",
|
|
361
|
+
alignment: { horizontal: :right }
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
date_style = sheet.styles.add_style(
|
|
365
|
+
num_fmt: "dd/mm/yyyy",
|
|
366
|
+
alignment: { horizontal: :center }
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Headers
|
|
370
|
+
sheet.add_row(
|
|
371
|
+
["ID", "Cliente", "Email", "Total", "Estado", "Fecha"],
|
|
372
|
+
style: header_style
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Datos
|
|
376
|
+
@orders.each do |order|
|
|
377
|
+
sheet.add_row(
|
|
378
|
+
[
|
|
379
|
+
order.id,
|
|
380
|
+
order.user.name,
|
|
381
|
+
order.user.email,
|
|
382
|
+
order.total,
|
|
383
|
+
order.status.humanize,
|
|
384
|
+
order.created_at
|
|
385
|
+
],
|
|
386
|
+
style: [nil, nil, nil, money_style, nil, date_style]
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Auto-ajustar columnas
|
|
391
|
+
sheet.column_widths 10, 25, 30, 15, 15, 15
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Controller
|
|
395
|
+
class OrdersController < ApplicationController
|
|
396
|
+
def index
|
|
397
|
+
@orders = Order.includes(:user).recent
|
|
398
|
+
|
|
399
|
+
respond_to do |format|
|
|
400
|
+
format.html
|
|
401
|
+
format.xlsx {
|
|
402
|
+
response.headers["Content-Disposition"] = "attachment; filename=pedidos-#{Date.current}.xlsx"
|
|
403
|
+
}
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Export con múltiples hojas
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# app/views/reports/monthly.xlsx.axlsx
|
|
413
|
+
wb = xlsx_package.workbook
|
|
414
|
+
|
|
415
|
+
# Hoja de resumen
|
|
416
|
+
wb.add_worksheet(name: "Resumen") do |sheet|
|
|
417
|
+
sheet.add_row ["Reporte Mensual - #{@month.strftime('%B %Y')}"]
|
|
418
|
+
sheet.add_row []
|
|
419
|
+
sheet.add_row ["Total Pedidos", @stats[:total_orders]]
|
|
420
|
+
sheet.add_row ["Ingresos", @stats[:revenue]]
|
|
421
|
+
sheet.add_row ["Ticket Medio", @stats[:average_ticket]]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Hoja de pedidos
|
|
425
|
+
wb.add_worksheet(name: "Pedidos") do |sheet|
|
|
426
|
+
sheet.add_row ["ID", "Cliente", "Total", "Fecha"]
|
|
427
|
+
@orders.each do |order|
|
|
428
|
+
sheet.add_row [order.id, order.user.name, order.total, order.created_at]
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Hoja de productos más vendidos
|
|
433
|
+
wb.add_worksheet(name: "Top Productos") do |sheet|
|
|
434
|
+
sheet.add_row ["Producto", "Unidades", "Ingresos"]
|
|
435
|
+
@top_products.each do |product|
|
|
436
|
+
sheet.add_row [product.name, product.units_sold, product.revenue]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Excel Import
|
|
442
|
+
|
|
443
|
+
### Con roo gem
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
# Gemfile
|
|
447
|
+
gem "roo"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
# app/services/excel_import_service.rb
|
|
452
|
+
class ExcelImportService
|
|
453
|
+
def initialize(file)
|
|
454
|
+
@spreadsheet = open_spreadsheet(file)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def import(model_class, sheet: 0, header_row: 1)
|
|
458
|
+
sheet = @spreadsheet.sheet(sheet)
|
|
459
|
+
headers = sheet.row(header_row).map(&:to_s).map(&:strip)
|
|
460
|
+
|
|
461
|
+
results = { success: 0, errors: [] }
|
|
462
|
+
|
|
463
|
+
((header_row + 1)..sheet.last_row).each do |i|
|
|
464
|
+
row = Hash[headers.zip(sheet.row(i))]
|
|
465
|
+
|
|
466
|
+
record = model_class.new(row)
|
|
467
|
+
if record.save
|
|
468
|
+
results[:success] += 1
|
|
469
|
+
else
|
|
470
|
+
results[:errors] << { row: i, errors: record.errors.full_messages }
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
results
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def preview(sheet: 0, header_row: 1, limit: 5)
|
|
478
|
+
sheet = @spreadsheet.sheet(sheet)
|
|
479
|
+
headers = sheet.row(header_row)
|
|
480
|
+
|
|
481
|
+
rows = []
|
|
482
|
+
((header_row + 1)..[header_row + limit, sheet.last_row].min).each do |i|
|
|
483
|
+
rows << Hash[headers.zip(sheet.row(i))]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
{ headers: headers, rows: rows }
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def sheets
|
|
490
|
+
@spreadsheet.sheets
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
private
|
|
494
|
+
|
|
495
|
+
def open_spreadsheet(file)
|
|
496
|
+
case File.extname(file.original_filename)
|
|
497
|
+
when ".csv"
|
|
498
|
+
Roo::CSV.new(file.path)
|
|
499
|
+
when ".xls"
|
|
500
|
+
Roo::Excel.new(file.path)
|
|
501
|
+
when ".xlsx"
|
|
502
|
+
Roo::Excelx.new(file.path)
|
|
503
|
+
else
|
|
504
|
+
raise "Formato no soportado: #{file.original_filename}"
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## JSON Export/Import
|
|
511
|
+
|
|
512
|
+
### Export
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
# app/controllers/api/exports_controller.rb
|
|
516
|
+
module Api
|
|
517
|
+
class ExportsController < ApplicationController
|
|
518
|
+
def users
|
|
519
|
+
@users = User.includes(:posts, :comments)
|
|
520
|
+
|
|
521
|
+
send_data(
|
|
522
|
+
@users.to_json(include: [:posts, :comments]),
|
|
523
|
+
filename: "users-export-#{Time.current.to_i}.json",
|
|
524
|
+
type: :json
|
|
525
|
+
)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Export personalizado con JBuilder
|
|
531
|
+
# app/views/api/exports/users.json.jbuilder
|
|
532
|
+
json.exported_at Time.current.iso8601
|
|
533
|
+
json.count @users.size
|
|
534
|
+
|
|
535
|
+
json.users @users do |user|
|
|
536
|
+
json.extract! user, :id, :name, :email, :created_at
|
|
537
|
+
json.posts_count user.posts.size
|
|
538
|
+
|
|
539
|
+
json.posts user.posts do |post|
|
|
540
|
+
json.extract! post, :id, :title, :published_at
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Import
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
# app/services/json_import_service.rb
|
|
549
|
+
class JsonImportService
|
|
550
|
+
def initialize(file)
|
|
551
|
+
@data = JSON.parse(file.read)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def import_users
|
|
555
|
+
results = { success: 0, errors: [] }
|
|
556
|
+
|
|
557
|
+
@data["users"].each_with_index do |user_data, index|
|
|
558
|
+
user = User.new(user_data.except("posts"))
|
|
559
|
+
|
|
560
|
+
if user.save
|
|
561
|
+
import_posts(user, user_data["posts"]) if user_data["posts"]
|
|
562
|
+
results[:success] += 1
|
|
563
|
+
else
|
|
564
|
+
results[:errors] << { index: index, errors: user.errors.full_messages }
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
results
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
private
|
|
572
|
+
|
|
573
|
+
def import_posts(user, posts_data)
|
|
574
|
+
posts_data.each do |post_data|
|
|
575
|
+
user.posts.create!(post_data)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Vista de importación con Stimulus
|
|
582
|
+
|
|
583
|
+
```erb
|
|
584
|
+
<%# app/views/imports/new.html.erb %>
|
|
585
|
+
<div data-controller="import"
|
|
586
|
+
data-import-preview-url-value="<%= preview_imports_path %>">
|
|
587
|
+
|
|
588
|
+
<h1>Importar datos</h1>
|
|
589
|
+
|
|
590
|
+
<%= form_with url: imports_path, method: :post, local: true, multipart: true do |f| %>
|
|
591
|
+
<div class="mb-4">
|
|
592
|
+
<label class="block text-sm font-medium text-gray-700">
|
|
593
|
+
Archivo CSV o Excel
|
|
594
|
+
</label>
|
|
595
|
+
<%= f.file_field :file,
|
|
596
|
+
accept: ".csv,.xlsx,.xls",
|
|
597
|
+
class: "mt-1 block w-full",
|
|
598
|
+
data: { action: "change->import#preview" } %>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<%# Preview %>
|
|
602
|
+
<div data-import-target="preview" class="hidden mb-4">
|
|
603
|
+
<h3 class="font-medium mb-2">Vista previa</h3>
|
|
604
|
+
<div class="overflow-x-auto">
|
|
605
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
606
|
+
<thead data-import-target="headers"></thead>
|
|
607
|
+
<tbody data-import-target="rows"></tbody>
|
|
608
|
+
</table>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<%# Errores de validación %>
|
|
613
|
+
<div data-import-target="errors" class="hidden mb-4 bg-red-50 p-4 rounded">
|
|
614
|
+
<h3 class="font-medium text-red-800 mb-2">Errores encontrados</h3>
|
|
615
|
+
<ul data-import-target="errorList" class="list-disc pl-5 text-red-700"></ul>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<div class="flex gap-4">
|
|
619
|
+
<%= f.submit "Importar",
|
|
620
|
+
class: "btn btn-primary",
|
|
621
|
+
data: { import_target: "submit" } %>
|
|
622
|
+
|
|
623
|
+
<button type="button"
|
|
624
|
+
class="btn btn-secondary"
|
|
625
|
+
data-action="import#validate"
|
|
626
|
+
data-import-target="validateBtn">
|
|
627
|
+
Validar primero
|
|
628
|
+
</button>
|
|
629
|
+
</div>
|
|
630
|
+
<% end %>
|
|
631
|
+
</div>
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
```javascript
|
|
635
|
+
// app/javascript/controllers/import_controller.js
|
|
636
|
+
import { Controller } from "@hotwired/stimulus"
|
|
637
|
+
|
|
638
|
+
export default class extends Controller {
|
|
639
|
+
static targets = ["preview", "headers", "rows", "errors", "errorList", "submit", "validateBtn"]
|
|
640
|
+
static values = { previewUrl: String }
|
|
641
|
+
|
|
642
|
+
async preview(event) {
|
|
643
|
+
const file = event.target.files[0]
|
|
644
|
+
if (!file) return
|
|
645
|
+
|
|
646
|
+
const formData = new FormData()
|
|
647
|
+
formData.append("file", file)
|
|
648
|
+
|
|
649
|
+
const response = await fetch(this.previewUrlValue, {
|
|
650
|
+
method: "POST",
|
|
651
|
+
body: formData,
|
|
652
|
+
headers: {
|
|
653
|
+
"X-CSRF-Token": document.querySelector("[name='csrf-token']").content
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
const data = await response.json()
|
|
658
|
+
this.showPreview(data)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
showPreview(data) {
|
|
662
|
+
// Headers
|
|
663
|
+
this.headersTarget.innerHTML = `
|
|
664
|
+
<tr>
|
|
665
|
+
${data.headers.map(h => `<th class="px-4 py-2 bg-gray-50">${h}</th>`).join("")}
|
|
666
|
+
</tr>
|
|
667
|
+
`
|
|
668
|
+
|
|
669
|
+
// Rows
|
|
670
|
+
this.rowsTarget.innerHTML = data.rows.map(row => `
|
|
671
|
+
<tr>
|
|
672
|
+
${data.headers.map(h => `<td class="px-4 py-2 border-t">${row[h] || ""}</td>`).join("")}
|
|
673
|
+
</tr>
|
|
674
|
+
`).join("")
|
|
675
|
+
|
|
676
|
+
this.previewTarget.classList.remove("hidden")
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async validate() {
|
|
680
|
+
// Implementar validación AJAX
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Export en background con notificación
|
|
686
|
+
|
|
687
|
+
```ruby
|
|
688
|
+
# app/jobs/export_job.rb
|
|
689
|
+
class ExportJob < ApplicationJob
|
|
690
|
+
queue_as :exports
|
|
691
|
+
|
|
692
|
+
def perform(export_id)
|
|
693
|
+
export = Export.find(export_id)
|
|
694
|
+
export.update!(status: "processing")
|
|
695
|
+
|
|
696
|
+
# Generar archivo
|
|
697
|
+
data = generate_export(export)
|
|
698
|
+
|
|
699
|
+
# Guardar archivo
|
|
700
|
+
export.file.attach(
|
|
701
|
+
io: StringIO.new(data),
|
|
702
|
+
filename: "#{export.export_type}-#{Date.current}.csv",
|
|
703
|
+
content_type: "text/csv"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
export.update!(status: "completed")
|
|
707
|
+
|
|
708
|
+
# Notificar vía Turbo Stream
|
|
709
|
+
Turbo::StreamsChannel.broadcast_update_to(
|
|
710
|
+
"exports_#{export.user_id}",
|
|
711
|
+
target: "export_#{export.id}",
|
|
712
|
+
partial: "exports/export",
|
|
713
|
+
locals: { export: export }
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# También por email
|
|
717
|
+
ExportMailer.ready(export).deliver_later
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
private
|
|
721
|
+
|
|
722
|
+
def generate_export(export)
|
|
723
|
+
case export.export_type
|
|
724
|
+
when "users"
|
|
725
|
+
User.to_csv
|
|
726
|
+
when "orders"
|
|
727
|
+
Order.where(created_at: export.date_range).to_csv
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
## Checklist
|
|
734
|
+
|
|
735
|
+
### Export
|
|
736
|
+
- [ ] Streaming para archivos grandes
|
|
737
|
+
- [ ] Formato de fechas consistente
|
|
738
|
+
- [ ] Encoding UTF-8 con BOM para Excel
|
|
739
|
+
- [ ] Headers descriptivos
|
|
740
|
+
- [ ] Nombre de archivo con fecha
|
|
741
|
+
|
|
742
|
+
### Import
|
|
743
|
+
- [ ] Validación de formato de archivo
|
|
744
|
+
- [ ] Preview antes de importar
|
|
745
|
+
- [ ] Validación de datos antes de guardar
|
|
746
|
+
- [ ] Manejo de errores por fila
|
|
747
|
+
- [ ] Transacción para rollback
|
|
748
|
+
- [ ] Background job para archivos grandes
|
|
749
|
+
- [ ] Notificación al completar
|
|
750
|
+
- [ ] Log de importaciones
|