your_ai_insight 1.0.7

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.
data/config/routes.rb ADDED
@@ -0,0 +1,14 @@
1
+ YourAiInsight::Engine.routes.draw do
2
+ # Admin dashboard UI
3
+ get "dashboard", to: "dashboard#index", as: :dashboard
4
+ get "dashboard/quick_context", to: "dashboard#quick_context", as: :dashboard_quick_context
5
+
6
+ # Chat (JSON API)
7
+ post "chat", to: "chats#create", as: :chat
8
+
9
+ # Reports (JSON API + HTML show)
10
+ resources :reports, only: [:index, :create, :show]
11
+
12
+ # Root → dashboard
13
+ root to: "dashboard#index"
14
+ end
@@ -0,0 +1,20 @@
1
+ class CreateYourAiInsightReports < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :your_ai_insight_reports do |t|
4
+ t.string :report_type, null: false
5
+ t.text :content, null: false
6
+ t.jsonb :raw_data, null: false, default: {}
7
+ t.date :period_start
8
+ t.date :period_end
9
+ t.boolean :scheduled, null: false, default: false
10
+ t.integer :generated_by_user_id # optional FK to your existing users table
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :your_ai_insight_reports, :report_type
16
+ add_index :your_ai_insight_reports, :created_at
17
+ add_index :your_ai_insight_reports, :scheduled
18
+ add_index :your_ai_insight_reports, :generated_by_user_id
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ namespace :your_ai_insight do
2
+ desc "Generate + email a Summary Dashboard report. Args: emails (comma-sep), days_back"
3
+ task :summary_dashboard, [:emails, :days_back] => :environment do |_, args|
4
+ emails = resolve_emails(args[:emails])
5
+ days_back = (args[:days_back] || ENV["DAYS_BACK"] || 30).to_i
6
+
7
+ abort_if_no_emails(emails)
8
+ puts "[YourAiInsight] Enqueuing summary_dashboard | #{days_back} days | → #{emails.join(', ')}"
9
+
10
+ YourAiInsight::ScheduledReportJob.new(
11
+ report_type: "summary_dashboard",
12
+ recipient_emails: emails,
13
+ days_back: days_back
14
+ ).enqueue
15
+
16
+ puts "[YourAiInsight] Done — check your Delayed::Job queue."
17
+ end
18
+
19
+ desc "Generate + email a Compliance & Audit report. Args: emails (comma-sep), days_back"
20
+ task :compliance_audit, [:emails, :days_back] => :environment do |_, args|
21
+ emails = resolve_emails(args[:emails])
22
+ days_back = (args[:days_back] || ENV["DAYS_BACK"] || 30).to_i
23
+
24
+ abort_if_no_emails(emails)
25
+ puts "[YourAiInsight] Enqueuing compliance_audit | #{days_back} days | → #{emails.join(', ')}"
26
+
27
+ YourAiInsight::ScheduledReportJob.new(
28
+ report_type: "compliance_audit",
29
+ recipient_emails: emails,
30
+ days_back: days_back
31
+ ).enqueue
32
+
33
+ puts "[YourAiInsight] Done."
34
+ end
35
+
36
+ desc "Run both reports immediately and print output (for testing)"
37
+ task :run_now => :environment do
38
+ emails = resolve_emails(ENV["REPORT_EMAILS"])
39
+ abort "[YourAiInsight] Set REPORT_EMAILS=you@example.com" if emails.empty?
40
+
41
+ %w[summary_dashboard compliance_audit].each do |type|
42
+ puts "\n[YourAiInsight] ── Running #{type} ──"
43
+ job = YourAiInsight::ScheduledReportJob.new(
44
+ report_type: type,
45
+ recipient_emails: emails,
46
+ days_back: 30
47
+ )
48
+ job.perform
49
+ puts "[YourAiInsight] #{type} complete."
50
+ end
51
+ end
52
+
53
+ def resolve_emails(raw)
54
+ raw ||= ENV["REPORT_EMAILS"] || ""
55
+ raw.split(/[,\s+]/).map(&:strip).reject(&:blank?)
56
+ end
57
+
58
+ def abort_if_no_emails(emails)
59
+ if emails.empty?
60
+ abort "[YourAiInsight] No recipients. Pass as arg or set REPORT_EMAILS=a@co.com,b@co.com"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,299 @@
1
+ ##
2
+ # lib/tasks/your_ai_insight_seed.rake
3
+ #
4
+ # Rake tasks for seeding test report data and backfilling historical reports.
5
+ #
6
+ # USAGE:
7
+ # rake your_ai_insight:seed # seed 10 realistic fake reports
8
+ # rake your_ai_insight:seed[20] # seed N reports
9
+ # rake your_ai_insight:backfill[30] # generate real AI reports for last N days
10
+ # rake your_ai_insight:backfill_dry_run[30] # preview what backfill would generate
11
+ # rake your_ai_insight:clear # delete all YourAiInsight reports (careful!)
12
+ # rake your_ai_insight:stats # print report DB stats
13
+ ##
14
+
15
+ namespace :your_ai_insight do
16
+
17
+ # ─────────────────────────────────────────────────────────────────────────
18
+ # SEED — insert realistic fake reports without calling Claude API
19
+ # Useful for UI development, testing views, and demoing to clients.
20
+ # ─────────────────────────────────────────────────────────────────────────
21
+ desc "Seed YourAiInsight with N realistic fake reports (default: 10). Does NOT call Claude API."
22
+ task :seed, [:count] => :environment do |_, args|
23
+ count = (args[:count] || 10).to_i
24
+ puts "[YourAiInsight] Seeding #{count} fake reports…"
25
+
26
+ count.times do |i|
27
+ type = i.even? ? "summary_dashboard" : "compliance_audit"
28
+ days_ago = rand(1..90)
29
+ period_end = days_ago.days.ago.to_date
30
+ period_start = (period_end - rand(7..30).days)
31
+ scheduled = [true, false].sample
32
+
33
+ content = type == "summary_dashboard" \
34
+ ? fake_dashboard_report(i) \
35
+ : fake_compliance_report(i)
36
+
37
+ report = YourAiInsight::AiReport.create!(
38
+ report_type: type,
39
+ content: content,
40
+ raw_data: fake_raw_data(type),
41
+ period_start: period_start,
42
+ period_end: period_end,
43
+ scheduled: scheduled,
44
+ created_at: days_ago.days.ago + rand(0..8).hours
45
+ )
46
+
47
+ puts " [#{i+1}/#{count}] #{type} ##{report.id} | #{period_start} – #{period_end} | #{scheduled ? 'scheduled' : 'on-demand'}"
48
+ end
49
+
50
+ puts "\n[YourAiInsight] ✓ Seeded #{count} reports."
51
+ puts " Visit: /your_ai_insight/dashboard to see them.\n"
52
+ end
53
+
54
+ # ─────────────────────────────────────────────────────────────────────────
55
+ # BACKFILL — generate REAL AI reports from live data for past N days
56
+ # WARNING: This calls the Claude API once per report. Cost applies.
57
+ # ─────────────────────────────────────────────────────────────────────────
58
+ desc "Backfill real AI reports for past N days using live data (calls Claude API — cost applies)"
59
+ task :backfill, [:days_back] => :environment do |_, args|
60
+ days_back = (args[:days_back] || 30).to_i
61
+
62
+ puts "[YourAiInsight] Backfilling real reports for past #{days_back} days…"
63
+ puts " WARNING: This calls the Claude API. Each report costs ~$0.01–$0.05."
64
+ puts " Press ENTER to continue or Ctrl+C to abort."
65
+ $stdin.gets
66
+
67
+ date_range = days_back.days.ago.beginning_of_day..Time.current
68
+ svc = YourAiInsight::FacilityDataService.new(date_range: date_range)
69
+
70
+ %w[summary_dashboard compliance_audit].each do |type|
71
+ puts "\n[YourAiInsight] Generating #{type}…"
72
+
73
+ data = type == "summary_dashboard" ? svc.summary_dashboard_data : svc.compliance_audit_data
74
+ content = YourAiInsight::ClaudeService.new.generate_report(type.to_sym, data)
75
+
76
+ report = YourAiInsight::AiReport.create!(
77
+ report_type: type,
78
+ content: content,
79
+ raw_data: data,
80
+ period_start: date_range.first.to_date,
81
+ period_end: date_range.last.to_date,
82
+ scheduled: false
83
+ )
84
+
85
+ puts " ✓ #{type} report ##{report.id} created (#{content.length} chars)"
86
+ end
87
+
88
+ puts "\n[YourAiInsight] Backfill complete. Visit /your_ai_insight/dashboard."
89
+ rescue YourAiInsight::ApiError => e
90
+ puts "\n[YourAiInsight] Claude API error: #{e.message}"
91
+ puts " Check your ANTHROPIC_API_KEY environment variable."
92
+ exit 1
93
+ rescue YourAiInsight::ConfigurationError => e
94
+ puts "\n[YourAiInsight] Configuration error: #{e.message}"
95
+ exit 1
96
+ end
97
+
98
+ # ─────────────────────────────────────────────────────────────────────────
99
+ # DRY RUN — shows what data would be sent to Claude without calling API
100
+ # ─────────────────────────────────────────────────────────────────────────
101
+ desc "Preview the data that would be sent to Claude for backfill (no API call)"
102
+ task :backfill_dry_run, [:days_back] => :environment do |_, args|
103
+ days_back = (args[:days_back] || 30).to_i
104
+ date_range = days_back.days.ago.beginning_of_day..Time.current
105
+ svc = YourAiInsight::FacilityDataService.new(date_range: date_range)
106
+
107
+ puts "[YourAiInsight] Dry-run backfill for past #{days_back} days\n"
108
+ puts "=" * 60
109
+
110
+ puts "\n── Summary Dashboard Data ──\n"
111
+ data = svc.summary_dashboard_data
112
+ puts JSON.pretty_generate(data)
113
+
114
+ puts "\n── Compliance Audit Data ──\n"
115
+ data = svc.compliance_audit_data
116
+ puts JSON.pretty_generate(data)
117
+
118
+ puts "\n[YourAiInsight] Dry run complete. No API calls were made."
119
+ end
120
+
121
+ # ─────────────────────────────────────────────────────────────────────────
122
+ # STATS — print report table stats
123
+ # ─────────────────────────────────────────────────────────────────────────
124
+ desc "Print YourAiInsight report statistics"
125
+ task stats: :environment do
126
+ total = YourAiInsight::AiReport.count
127
+ dashboards = YourAiInsight::AiReport.dashboards.count
128
+ compliance = YourAiInsight::AiReport.compliance.count
129
+ scheduled = YourAiInsight::AiReport.scheduled.count
130
+ on_demand = YourAiInsight::AiReport.on_demand.count
131
+ newest = YourAiInsight::AiReport.recent.first
132
+ oldest = YourAiInsight::AiReport.order(:created_at).first
133
+
134
+ puts "\n[YourAiInsight] Report Statistics"
135
+ puts " Total reports: #{total}"
136
+ puts " Summary dashboards: #{dashboards}"
137
+ puts " Compliance audits: #{compliance}"
138
+ puts " Scheduled: #{scheduled}"
139
+ puts " On-demand: #{on_demand}"
140
+ puts " Newest: #{newest&.created_at&.strftime('%b %d, %Y %I:%M %p') || 'none'}"
141
+ puts " Oldest: #{oldest&.created_at&.strftime('%b %d, %Y %I:%M %p') || 'none'}"
142
+ puts ""
143
+ end
144
+
145
+ # ─────────────────────────────────────────────────────────────────────────
146
+ # CLEAR — delete all reports (use with care)
147
+ # ─────────────────────────────────────────────────────────────────────────
148
+ desc "Delete ALL YourAiInsight reports from the database (IRREVERSIBLE)"
149
+ task clear: :environment do
150
+ count = YourAiInsight::AiReport.count
151
+ puts "[YourAiInsight] This will permanently delete #{count} reports."
152
+ puts " Type 'yes' to confirm or press Ctrl+C to abort:"
153
+ input = $stdin.gets.strip
154
+
155
+ if input.downcase == "yes"
156
+ YourAiInsight::AiReport.delete_all
157
+ puts "[YourAiInsight] ✓ Deleted #{count} reports."
158
+ else
159
+ puts "[YourAiInsight] Aborted."
160
+ end
161
+ end
162
+
163
+ # ─────────────────────────────────────────────────────────────────────────
164
+ # HELPERS — realistic fake report content
165
+ # ─────────────────────────────────────────────────────────────────────────
166
+
167
+ def fake_dashboard_report(seed)
168
+ scores = [72, 68, 81, 55, 88, 76, 63, 91, 70, 84]
169
+ score = scores[seed % scores.length]
170
+ jobs = rand(18..62)
171
+ hold = rand(0..6)
172
+ overdue = rand(0..14)
173
+ budget = rand(120_000..450_000)
174
+ actual = (budget * rand(0.75..1.15)).round
175
+
176
+ <<~REPORT
177
+ ## Overall Health Score: #{score}/100
178
+ Operations are #{score >= 80 ? 'performing well with minor areas to watch' : score >= 65 ? 'generally on track with several items requiring attention' : 'below target — immediate action recommended on several fronts'}.
179
+
180
+ ## Key Metrics at a Glance
181
+ - **Active jobs:** #{jobs} (#{hold} on hold)
182
+ - **Overdue tasks:** #{overdue}
183
+ - **Budget utilization:** $#{actual.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse} of $#{budget.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse} (#{((actual.to_f/budget)*100).round(1)}%)
184
+ - **Open bids awaiting decision:** #{rand(2..8)}
185
+ - **Pending budget requests:** #{rand(0..5)}
186
+
187
+ ## Job Pipeline Status
188
+ - **New:** #{rand(2..8)} jobs awaiting assignment
189
+ - **Active:** #{rand(8..20)} jobs currently in progress
190
+ - **Work Complete:** #{rand(3..10)} jobs pending final sign-off
191
+ - **On Hold:** #{hold} jobs — #{hold > 3 ? '⚠️ elevated hold count, review required' : 'within acceptable range'}
192
+ - **Priority breakdown:** #{rand(3..8)} Emergency / #{rand(5..12)} High / #{rand(10..25)} Normal
193
+
194
+ ## Budget & Cost Performance
195
+ - Total expense budget vs actual: #{actual > budget ? '**over budget by $' + (actual-budget).to_s + '** — investigate largest line items' : 'under budget by $' + (budget-actual).to_s + ' — on track'}
196
+ - **#{rand(1..4)} jobs** are running more than 15% over their task budgets
197
+ - Largest expense category: #{['HVAC', 'Electrical', 'Plumbing', 'General Maintenance'].sample}
198
+ - Average task cost variance: #{rand(-8..22)}%
199
+
200
+ ## Vendor & Bid Activity
201
+ - **#{rand(3..10)} bids** pending decision (oldest: #{rand(3..21)} days old)
202
+ - **#{rand(1..6)} pay requests** outstanding from vendors
203
+ - Top vendor by task count: #{['Allied Mechanical', 'ProElectric LLC', 'Summit HVAC', 'Reliable Plumbing'].sample}
204
+
205
+ ## Highlights
206
+ - #{rand(5..12)} jobs successfully finaled this period
207
+ - #{rand(60..85)}% of tasks completed on or before due date
208
+ - Budget requests processed within #{rand(2..7)} days average
209
+
210
+ ## Concerns
211
+ #{overdue > 5 ? '- ⚠️ **High overdue task count** — ' + overdue.to_s + ' tasks past due date' : '- **' + overdue.to_s + ' overdue tasks** — manageable but monitor closely'}
212
+ #{hold > 3 ? '- ⚠️ **' + hold.to_s + ' jobs on hold** — review individually for blockers' : ''}
213
+ #{actual > budget ? '- **Expense actuals exceed budget** — identify cost drivers' : ''}
214
+
215
+ ## Top 5 Recommended Actions
216
+ 1. **Review #{hold} held jobs** — determine blockers and set target release dates
217
+ 2. **Resolve #{overdue} overdue tasks** — assign responsible parties and new due dates
218
+ 3. **Action pending bids** — #{rand(3..10)} bids over 5 days old without a decision
219
+ 4. **Approve outstanding budget requests** — #{rand(0..5)} pending approval for over a week
220
+ 5. **Process outstanding pay requests** — #{rand(1..6)} vendor pay requests awaiting approval
221
+ REPORT
222
+ end
223
+
224
+ def fake_compliance_report(seed)
225
+ overdue_tasks = rand(2..15)
226
+ held_jobs = rand(0..8)
227
+ pending_budget = rand(0..6)
228
+ open_bids = rand(1..9)
229
+ pay_reqs = rand(0..5)
230
+
231
+ <<~REPORT
232
+ ## Compliance Status Overview
233
+ Review period contains **#{overdue_tasks} overdue tasks**, **#{held_jobs} jobs on hold**, and **#{pending_budget} pending budget requests** requiring approval.
234
+ #{overdue_tasks > 8 || held_jobs > 5 ? '**Status: REQUIRES IMMEDIATE ATTENTION**' : '**Status: Monitor and action within 14 days**'}
235
+
236
+ ## ⚠️ Critical Items (Immediate Action Required)
237
+
238
+ #{held_jobs > 0 ? "### Jobs on Hold (#{held_jobs} total)\n" + held_jobs.times.map { |i| "- **Job ##{rand(1000..9999)}** — #{['Awaiting customer approval', 'Pending vendor availability', 'Budget request outstanding', 'Parts on order'].sample} (#{rand(3..45)} days on hold)" }.join("\n") : "No jobs currently on hold."}
239
+
240
+ #{overdue_tasks > 5 ? "### ⚠️ High Overdue Task Count\n- **#{overdue_tasks} tasks** are past their due date — this is above the acceptable threshold of 5\n- Oldest overdue task: #{rand(7..35)} days past due\n- Locations affected: #{rand(2..6)} sites" : ""}
241
+
242
+ ## ⚡ Warnings (Action Required Within 30 Days)
243
+
244
+ ### Pending Budget Requests (#{pending_budget})
245
+ #{pending_budget > 0 ? pending_budget.times.map { |i| "- Request ##{rand(100..999)}: **$#{rand(500..15000).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}** — #{rand(3..28)} days pending — #{['HVAC repair', 'Electrical upgrade', 'Roof repair', 'Plumbing emergency', 'Equipment replacement'].sample}" }.join("\n") : "No pending budget requests."}
246
+
247
+ ### Open Bids (#{open_bids} without decision)
248
+ #{open_bids.times.map { |i| "- Bid ##{rand(200..999)}: **$#{rand(1200..35000).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}** from #{['Allied Mechanical', 'ProElectric LLC', 'Summit HVAC', 'Reliable Plumbing', 'CoreTech Services'].sample} — received #{rand(4..30)} days ago" }.join("\n")}
249
+
250
+ ## Budget Request Audit
251
+ - **#{pending_budget} requests** currently pending approval
252
+ - Total pending amount: **$#{(pending_budget * rand(2000..8000)).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}**
253
+ - Average days pending: #{rand(5..18)} days
254
+ - #{pending_budget > 3 ? '⚡ WARNING: Multiple requests aging past 14 days — escalate to approvers' : 'Approval pace is acceptable'}
255
+
256
+ ## Job Hold Audit
257
+ #{held_jobs > 0 ? "- #{held_jobs} jobs currently on hold\n- Longest held: #{rand(5..60)} days\n- Estimated revenue impact: $#{(held_jobs * rand(5000..20000)).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}" : "- No jobs currently on hold ✓"}
258
+
259
+ ## Vendor Pay Request Audit
260
+ - **#{pay_reqs} pay requests** outstanding (not yet paid or declined)
261
+ #{pay_reqs > 0 ? "- Total outstanding: **$#{(pay_reqs * rand(3000..12000)).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}**\n- Oldest: #{rand(3..21)} days pending" : "- All pay requests have been actioned ✓"}
262
+
263
+ ## Completed This Period
264
+ - ✓ #{rand(8..20)} jobs finaled
265
+ - ✓ #{rand(4..12)} budget requests approved
266
+ - ✓ #{rand(5..18)} pay requests processed
267
+
268
+ ## Recommendations
269
+ 1. **Immediately review held jobs** — schedule daily stand-up until all hold reasons are resolved
270
+ 2. **Clear overdue tasks** — assign a team lead to contact vendors/assignees for status updates
271
+ 3. **Batch-process pending budget requests** — hold weekly approval meeting to clear backlog
272
+ 4. **Respond to open bids** — bids older than 14 days may cause vendor relationships issues
273
+ 5. **Process outstanding pay requests** — unpaid vendor invoices risk future service disruptions
274
+ REPORT
275
+ end
276
+
277
+ def fake_raw_data(type)
278
+ if type == "summary_dashboard"
279
+ {
280
+ period: { from: 30.days.ago.to_s, to: Time.current.to_s },
281
+ jobs: { total: rand(25..80), on_hold: rand(0..8), by_status: { "active" => rand(10..30), "new" => rand(3..10), "finaled" => rand(5..20) } },
282
+ tasks: { total: rand(40..120), open: rand(15..50), overdue: rand(0..15), total_budget: rand(50000..300000).to_f, total_actual: rand(45000..320000).to_f },
283
+ bids: { total: rand(10..40), pending: rand(2..10), accepted: rand(5..20) },
284
+ expenses: { total_budget: rand(100000..500000).to_f, total_actual: rand(90000..520000).to_f },
285
+ budget_requests: { total: rand(5..20), pending: rand(0..6), approved: rand(3..12) }
286
+ }
287
+ else
288
+ {
289
+ period: { from: 30.days.ago.to_s, to: Time.current.to_s },
290
+ jobs_on_hold: rand(0..8).times.map { { id: rand(1000..9999), name: "Job #{rand(1000..9999)}", status: rand(0..3) } },
291
+ overdue_tasks: rand(2..15).times.map { { id: rand(100..999), name: "Task #{rand(100..999)}", complete: rand(5..30).days.ago.to_date } },
292
+ pending_budget_requests: rand(0..5).times.map { { id: rand(10..99), amount: rand(500..15000).to_f } },
293
+ open_bids_no_decision: rand(1..9).times.map { { id: rand(100..999), amount: rand(1000..35000).to_f } },
294
+ outstanding_pay_requests:rand(0..5).times.map { { id: rand(10..99), amount: rand(2000..15000).to_f } }
295
+ }
296
+ end
297
+ end
298
+
299
+ end
@@ -0,0 +1,168 @@
1
+ module YourAiInsight
2
+ class Configuration
3
+ # ── Required ──────────────────────────────────────────────────────────────
4
+ attr_accessor :anthropic_api_key
5
+
6
+ # ── Model / Claude settings ───────────────────────────────────────────────
7
+ attr_accessor :claude_model # default: claude-sonnet-4-20250514
8
+ attr_accessor :max_tokens # default: 2048
9
+
10
+ # ── Multi-tenancy: scope data to current user's locations/customers ───────
11
+ # Proc receives current_user and returns an Array of location_ids, or nil for all.
12
+ # Example: config.location_scope = ->(user) { user.locations.pluck(:id) }
13
+ attr_accessor :location_scope
14
+
15
+ # Proc receives current_user and returns an Array of customer_ids, or nil for all.
16
+ attr_accessor :customer_scope
17
+
18
+ # ── Branding ──────────────────────────────────────────────────────────────
19
+ attr_accessor :company_name
20
+ attr_accessor :logo_url
21
+
22
+ # ── Scheduled report recipients ───────────────────────────────────────────
23
+ attr_accessor :default_recipients
24
+
25
+ # ── Schema mapping — pre-filled to match portal.allproifm.com schema.rb ──
26
+ # Only change these if you're installing in a different app with different names.
27
+
28
+ # jobs table
29
+ attr_accessor :jobs_table # "jobs"
30
+ attr_accessor :jobs_status_col # "status" (integer enum)
31
+ attr_accessor :jobs_priority_col # "priority"
32
+ attr_accessor :jobs_sales_price_col # "sales_price"
33
+ attr_accessor :jobs_not_to_exceed_col # "not_to_exceed"
34
+ attr_accessor :jobs_location_id_col # "location_id"
35
+ attr_accessor :jobs_customer_id_col # "customer_id"
36
+ attr_accessor :jobs_accepted_dt_col # "accepted_dt"
37
+ attr_accessor :jobs_finaled_dt_col # "finaled_dt"
38
+ attr_accessor :jobs_hold_col # "hold"
39
+
40
+ # tasks table
41
+ attr_accessor :tasks_table # "tasks"
42
+ attr_accessor :tasks_status_col # "status" (integer enum)
43
+ attr_accessor :tasks_budget_col # "budget"
44
+ attr_accessor :tasks_actual_col # "actual"
45
+ attr_accessor :tasks_job_id_col # "job_id"
46
+ attr_accessor :tasks_complete_col # "complete" (date)
47
+ attr_accessor :tasks_start_col # "start" (date)
48
+
49
+ # bids table
50
+ attr_accessor :bids_table # "bids"
51
+ attr_accessor :bids_status_col # "status"
52
+ attr_accessor :bids_amount_col # "amount"
53
+ attr_accessor :bids_actual_col # "actual"
54
+ attr_accessor :bids_task_id_col # "task_id"
55
+
56
+ # expenses table
57
+ attr_accessor :expenses_table # "expenses"
58
+ attr_accessor :expenses_budget_col # "budget"
59
+ attr_accessor :expenses_actual_col # "actual"
60
+ attr_accessor :expenses_job_id_col # "job_id"
61
+ attr_accessor :expenses_task_id_col # "task_id"
62
+
63
+ # location_budgets table
64
+ attr_accessor :location_budgets_table # "location_budgets"
65
+ attr_accessor :lb_location_id_col # "location_id"
66
+ attr_accessor :lb_fiscal_year_col # "fiscal_year"
67
+ # month1..month12 columns are assumed by name
68
+
69
+ # locations table
70
+ attr_accessor :locations_table # "locations"
71
+ attr_accessor :locations_name_col # "name"
72
+ attr_accessor :locations_customer_id_col# "customer_id"
73
+
74
+ # customers table
75
+ attr_accessor :customers_table # "customers"
76
+ attr_accessor :customers_name_col # "name"
77
+ attr_accessor :customers_status_col # "status"
78
+
79
+ # budget_requests table
80
+ attr_accessor :budget_requests_table # "budget_requests"
81
+ attr_accessor :br_status_col # "status" (integer enum)
82
+ attr_accessor :br_amount_col # "amount"
83
+ attr_accessor :br_task_id_col # "task_id"
84
+
85
+ # vendors / sub_contractors
86
+ attr_accessor :vendors_table # "vendors"
87
+ attr_accessor :sub_contractors_table # "sub_contractors"
88
+
89
+ def initialize
90
+ @claude_model = "claude-sonnet-4-20250514"
91
+ @max_tokens = 2048
92
+ @company_name = "AllPro IFM"
93
+ @logo_url = nil
94
+ @default_recipients = []
95
+
96
+ # jobs
97
+ @jobs_table = "jobs"
98
+ @jobs_status_col = "status"
99
+ @jobs_priority_col = "priority"
100
+ @jobs_sales_price_col = "sales_price"
101
+ @jobs_not_to_exceed_col = "not_to_exceed"
102
+ @jobs_location_id_col = "location_id"
103
+ @jobs_customer_id_col = "customer_id"
104
+ @jobs_accepted_dt_col = "accepted_dt"
105
+ @jobs_finaled_dt_col = "finaled_dt"
106
+ @jobs_hold_col = "hold"
107
+
108
+ # tasks
109
+ @tasks_table = "tasks"
110
+ @tasks_status_col = "status"
111
+ @tasks_budget_col = "budget"
112
+ @tasks_actual_col = "actual"
113
+ @tasks_job_id_col = "job_id"
114
+ @tasks_complete_col = "complete"
115
+ @tasks_start_col = "start"
116
+
117
+ # bids
118
+ @bids_table = "bids"
119
+ @bids_status_col = "status"
120
+ @bids_amount_col = "amount"
121
+ @bids_actual_col = "actual"
122
+ @bids_task_id_col = "task_id"
123
+
124
+ # expenses
125
+ @expenses_table = "expenses"
126
+ @expenses_budget_col = "budget"
127
+ @expenses_actual_col = "actual"
128
+ @expenses_job_id_col = "job_id"
129
+ @expenses_task_id_col= "task_id"
130
+
131
+ # location_budgets
132
+ @location_budgets_table = "location_budgets"
133
+ @lb_location_id_col = "location_id"
134
+ @lb_fiscal_year_col = "fiscal_year"
135
+
136
+ # locations
137
+ @locations_table = "locations"
138
+ @locations_name_col = "name"
139
+ @locations_customer_id_col = "customer_id"
140
+
141
+ # customers
142
+ @customers_table = "customers"
143
+ @customers_name_col = "name"
144
+ @customers_status_col = "status"
145
+
146
+ # budget_requests
147
+ @budget_requests_table = "budget_requests"
148
+ @br_status_col = "status"
149
+ @br_amount_col = "amount"
150
+ @br_task_id_col = "task_id"
151
+
152
+ # vendors
153
+ @vendors_table = "vendors"
154
+ @sub_contractors_table = "sub_contractors"
155
+ end
156
+ end
157
+
158
+ class << self
159
+ def configuration
160
+ @configuration ||= Configuration.new
161
+ end
162
+ alias config configuration
163
+
164
+ def configure
165
+ yield(configuration)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,17 @@
1
+ require "your_ai_insight/version"
2
+ require "your_ai_insight/configuration"
3
+
4
+ module YourAiInsight
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace YourAiInsight
7
+
8
+ initializer "your_ai_insight.assets" do |app|
9
+ app.config.assets.paths << root.join("app/assets/stylesheets") rescue nil
10
+ app.config.assets.paths << root.join("app/assets/javascripts") rescue nil
11
+ end
12
+ end
13
+
14
+ # Custom errors
15
+ class ApiError < StandardError; end
16
+ class ConfigurationError < StandardError; end
17
+ end
@@ -0,0 +1,3 @@
1
+ module YourAiInsight
2
+ VERSION = "1.0.7"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "your_ai_insight/version"
2
+ require "your_ai_insight/configuration"
3
+ require "your_ai_insight/engine"