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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +284 -0
- data/app/controllers/your_ai_insight/application_controller.rb +35 -0
- data/app/controllers/your_ai_insight/chats_controller.rb +26 -0
- data/app/controllers/your_ai_insight/dashboard_controller.rb +25 -0
- data/app/controllers/your_ai_insight/reports_controller.rb +64 -0
- data/app/jobs/your_ai_insight/scheduled_report_job.rb +98 -0
- data/app/mailers/your_ai_insight/ai_report_mailer.rb +80 -0
- data/app/models/your_ai_insight/ai_report.rb +24 -0
- data/app/services/your_ai_insight/claude_service.rb +175 -0
- data/app/services/your_ai_insight/facility_data_service.rb +449 -0
- data/app/views/your_ai_insight/chat/_widget.html.erb +153 -0
- data/app/views/your_ai_insight/dashboard/index.html.erb +818 -0
- data/config/routes.rb +14 -0
- data/db/migrate/20240101000001_create_your_ai_insight_reports.rb +20 -0
- data/lib/tasks/your_ai_insight.rake +63 -0
- data/lib/tasks/your_ai_insight_seed.rake +299 -0
- data/lib/your_ai_insight/configuration.rb +168 -0
- data/lib/your_ai_insight/engine.rb +17 -0
- data/lib/your_ai_insight/version.rb +3 -0
- data/lib/your_ai_insight.rb +3 -0
- metadata +132 -0
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
|