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.
Files changed (111) hide show
  1. package/README.md +128 -0
  2. package/bin/claude-framework +3 -0
  3. package/framework/agents/design-lead.md +240 -0
  4. package/framework/agents/product-owner.md +179 -0
  5. package/framework/agents/tech-lead.md +226 -0
  6. package/framework/commands/ayuda.md +127 -0
  7. package/framework/commands/a/303/261adir.md +98 -0
  8. package/framework/commands/backup.md +397 -0
  9. package/framework/commands/cambiar.md +110 -0
  10. package/framework/commands/cloud.md +457 -0
  11. package/framework/commands/code.md +142 -0
  12. package/framework/commands/debug.md +334 -0
  13. package/framework/commands/deploy.md +383 -0
  14. package/framework/commands/deshacer.md +120 -0
  15. package/framework/commands/estado.md +218 -0
  16. package/framework/commands/explica.md +227 -0
  17. package/framework/commands/feature.md +120 -0
  18. package/framework/commands/git.md +427 -0
  19. package/framework/commands/historial.md +202 -0
  20. package/framework/commands/learn.md +408 -0
  21. package/framework/commands/movil.md +245 -0
  22. package/framework/commands/nuevo.md +118 -0
  23. package/framework/commands/plan.md +134 -0
  24. package/framework/commands/prd.md +113 -0
  25. package/framework/commands/probar.md +148 -0
  26. package/framework/commands/revisar.md +208 -0
  27. package/framework/commands/seeds.md +230 -0
  28. package/framework/commands/seguridad.md +226 -0
  29. package/framework/commands/tasks.md +157 -0
  30. package/framework/skills/architecture/algorithms.md +970 -0
  31. package/framework/skills/architecture/clean-code.md +1080 -0
  32. package/framework/skills/architecture/design-patterns.md +1984 -0
  33. package/framework/skills/architecture/functional-programming.md +972 -0
  34. package/framework/skills/architecture/solid.md +991 -0
  35. package/framework/skills/cloud/cloud-aws.md +848 -0
  36. package/framework/skills/cloud/cloud-azure.md +931 -0
  37. package/framework/skills/cloud/cloud-gcp.md +848 -0
  38. package/framework/skills/cloud/message-queues.md +1229 -0
  39. package/framework/skills/core/accessibility.md +401 -0
  40. package/framework/skills/core/api.md +474 -0
  41. package/framework/skills/core/authentication.md +306 -0
  42. package/framework/skills/core/authorization.md +388 -0
  43. package/framework/skills/core/background-jobs.md +341 -0
  44. package/framework/skills/core/caching.md +473 -0
  45. package/framework/skills/core/code-review.md +341 -0
  46. package/framework/skills/core/controllers.md +290 -0
  47. package/framework/skills/core/cua.md +285 -0
  48. package/framework/skills/core/documentation.md +472 -0
  49. package/framework/skills/core/file-uploads.md +351 -0
  50. package/framework/skills/core/hotwire-native.md +296 -0
  51. package/framework/skills/core/hotwire.md +278 -0
  52. package/framework/skills/core/i18n.md +334 -0
  53. package/framework/skills/core/imports-exports.md +750 -0
  54. package/framework/skills/core/infrastructure.md +337 -0
  55. package/framework/skills/core/models.md +228 -0
  56. package/framework/skills/core/notifications.md +672 -0
  57. package/framework/skills/core/payments.md +581 -0
  58. package/framework/skills/core/performance.md +361 -0
  59. package/framework/skills/core/rails-scaffold.md +131 -0
  60. package/framework/skills/core/search.md +518 -0
  61. package/framework/skills/core/security.md +565 -0
  62. package/framework/skills/core/seeds.md +307 -0
  63. package/framework/skills/core/seo.md +542 -0
  64. package/framework/skills/core/testing.md +393 -0
  65. package/framework/skills/core/views.md +260 -0
  66. package/framework/skills/core/websockets.md +564 -0
  67. package/framework/skills/data/advanced-sql.md +1204 -0
  68. package/framework/skills/data/nosql.md +1141 -0
  69. package/framework/skills/devops/containers-advanced.md +1237 -0
  70. package/framework/skills/devops/debugging.md +834 -0
  71. package/framework/skills/devops/git-workflow.md +752 -0
  72. package/framework/skills/devops/networking.md +932 -0
  73. package/framework/skills/devops/shell-scripting.md +1132 -0
  74. package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
  75. package/framework/sub-agents/cloud-agent.md +677 -0
  76. package/framework/sub-agents/data.md +504 -0
  77. package/framework/sub-agents/debugging-agent.md +554 -0
  78. package/framework/sub-agents/devops.md +483 -0
  79. package/framework/sub-agents/docs.md +176 -0
  80. package/framework/sub-agents/frontend-dev.md +349 -0
  81. package/framework/sub-agents/git-workflow-agent.md +697 -0
  82. package/framework/sub-agents/integrations.md +630 -0
  83. package/framework/sub-agents/native-dev.md +434 -0
  84. package/framework/sub-agents/qa.md +138 -0
  85. package/framework/sub-agents/rails-dev.md +375 -0
  86. package/framework/sub-agents/security.md +526 -0
  87. package/framework/sub-agents/ui.md +437 -0
  88. package/framework/sub-agents/ux.md +284 -0
  89. package/framework/templates/api-spec.md +500 -0
  90. package/framework/templates/component-spec.md +248 -0
  91. package/framework/templates/feature.json +13 -0
  92. package/framework/templates/model-spec.md +318 -0
  93. package/framework/templates/prd-template.md +80 -0
  94. package/framework/templates/task-plan.md +122 -0
  95. package/framework/templates/task-user-story.md +52 -0
  96. package/framework/templates/technical-spec.md +260 -0
  97. package/framework/templates/user-story.md +95 -0
  98. package/package.json +42 -0
  99. package/project-templates/CLAUDE.md +42 -0
  100. package/project-templates/contexts/architecture.md +25 -0
  101. package/project-templates/contexts/conventions.md +46 -0
  102. package/project-templates/contexts/design-system.md +47 -0
  103. package/project-templates/contexts/requirements.md +38 -0
  104. package/project-templates/contexts/stack.md +30 -0
  105. package/project-templates/history/active/models.md +11 -0
  106. package/project-templates/history/changelog.md +15 -0
  107. package/project-templates/workspace/.gitkeep +0 -0
  108. package/src/cli.js +52 -0
  109. package/src/init.js +104 -0
  110. package/src/status.js +75 -0
  111. 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