From 7a96c96fd62e770260a90fbf6688b309cd73043b Mon Sep 17 00:00:00 2001 From: ntxtthomas Date: Tue, 3 Feb 2026 22:06:29 -0600 Subject: [PATCH 1/4] UX mods, new role_types --- app/assets/stylesheets/application.css | 20 ++--------- app/controllers/dashboard_controller.rb | 4 --- app/models/concerns/role_types/other.rb | 15 ++++++++ .../concerns/role_types/support_engineer.rb | 32 +++++++++++++++++ app/models/opportunity.rb | 14 +++++++- app/views/dashboard/index.html.erb | 29 +++++++++------ app/views/opportunities/_form.html.erb | 21 ++++++----- app/views/opportunities/_opportunity.html.erb | 6 +++- .../_opportunity_details.html.erb | 20 ++++++++++- .../opportunities/_other_fields.html.erb | 5 +++ .../_support_engineer_fields.html.erb | 5 +++ app/views/opportunities/index.html.erb | 12 +++++++ ...03130000_normalize_opportunity_statuses.rb | 21 +++++++++++ db/schema.rb | 2 +- lib/tasks/update_proptech_industry.rake | 36 +++++++++++++++++++ 15 files changed, 198 insertions(+), 44 deletions(-) create mode 100644 app/models/concerns/role_types/other.rb create mode 100644 app/models/concerns/role_types/support_engineer.rb create mode 100644 app/views/opportunities/_other_fields.html.erb create mode 100644 app/views/opportunities/_support_engineer_fields.html.erb create mode 100644 db/migrate/20260203130000_normalize_opportunity_statuses.rb create mode 100644 lib/tasks/update_proptech_industry.rake diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index edbb54a..8d670dc 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -173,31 +173,15 @@ th a:hover { } /* Status styling */ -.status-submitted { +.status-applied { color: #0d6efd; } -.status-under_review { - color: #fd7e14; -} -.status-phone_screen { - color: #20c997; -} -.status-interview { +.status-interviewing { color: #6f42c1; } -.status-responded { - color: #198754; -} -.status-offer { - color: #198754; - font-weight: bold; -} .status-closed { color: #dc3545; } -.status-withdrawn { - color: #6c757d; -} /* Dashboard Cards */ .dashboard-cards { diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4e2d45e..6f6050d 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,10 +1,6 @@ class DashboardController < ApplicationController def index - @total_potentials = Opportunity.where(application_date: nil).count @total_resumes = Opportunity.where.not(application_date: nil).count - @total_responses = Opportunity.where(status: "responded").count - @total_interviews = Opportunity.where(status: "interview").count - @total_offers = Opportunity.where(status: "offer").count @total_assessed = Opportunity.count # Tech stack analytics diff --git a/app/models/concerns/role_types/other.rb b/app/models/concerns/role_types/other.rb new file mode 100644 index 0000000..b24750d --- /dev/null +++ b/app/models/concerns/role_types/other.rb @@ -0,0 +1,15 @@ +module RoleTypes + module Other + extend ActiveSupport::Concern + + # Other role metadata structure can be anything that doesn't fit the defined roles. + + included do + validate :validate_other_role_metadata, if: -> { role_type == "other" } + end + + def validate_other_role_metadata + # No required metadata for "other" + end + end +end diff --git a/app/models/concerns/role_types/support_engineer.rb b/app/models/concerns/role_types/support_engineer.rb new file mode 100644 index 0000000..8674df4 --- /dev/null +++ b/app/models/concerns/role_types/support_engineer.rb @@ -0,0 +1,32 @@ +module RoleTypes + module SupportEngineer + extend ActiveSupport::Concern + + # Support Engineer specific metadata structure: + # { + # support_tier: "l2", # l1, l2, l3 + # on_call: true, + # ticket_volume: "high", # low, medium, high + # escalation_path: "engineering", # engineering, product, customer_success + # support_tools: ["zendesk", "jira"], + # shift_type: "rotating" # fixed, rotating, follow_the_sun + # } + + included do + validate :validate_support_engineer_metadata, if: -> { role_type == "support_engineer" } + end + + def validate_support_engineer_metadata + # Add any specific validations for support engineer roles + end + + def support_summary + return "Not specified" unless role_metadata["support_tier"].present? + + tier = role_metadata["support_tier"].upcase + on_call = role_metadata["on_call"] ? "On-call" : nil + + ["Tier #{tier}", on_call].compact.join(" • ") + end + end +end diff --git a/app/models/opportunity.rb b/app/models/opportunity.rb index 2c32458..4a8d69b 100644 --- a/app/models/opportunity.rb +++ b/app/models/opportunity.rb @@ -4,6 +4,8 @@ class Opportunity < ApplicationRecord include RoleTypes::SalesEngineer include RoleTypes::SolutionsEngineer include RoleTypes::ProductManager + include RoleTypes::SupportEngineer + include RoleTypes::Other belongs_to :company has_many :opportunity_technologies, dependent: :destroy @@ -19,7 +21,9 @@ class Opportunity < ApplicationRecord "software_engineer" => "Software Engineer", "sales_engineer" => "Sales Engineer", "solutions_engineer" => "Solutions Engineer", - "product_manager" => "Product Manager" + "product_manager" => "Product Manager", + "support_engineer" => "Support Engineer", + "other" => "Other" }.freeze validates :role_type, presence: true, inclusion: { in: ROLE_TYPES.keys } @@ -48,6 +52,14 @@ def product_manager? role_type == "product_manager" end + def support_engineer? + role_type == "support_engineer" + end + + def other? + role_type == "other" + end + # Get/Set role metadata with indifferent access def metadata @metadata ||= (role_metadata || {}).with_indifferent_access diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index e2e2090..dd55577 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -64,22 +64,31 @@ <% end %> - -

Applications by Week

-
- <% @weekly_data.each_with_index do |week, index| %> - <%= link_to opportunities_path(start_date: week[:start_date], end_date: week[:end_date]), class: "card card-week clickable-week-card", style: "text-decoration: none; color: inherit;" do %> +
+
-

<%= week[:label] %>

+

Applications by Week

šŸ“…
-
<%= week[:count] %>
-

applications

+
+ <% @weekly_data.each_with_index do |week, index| %> + <%= link_to opportunities_path(start_date: week[:start_date], end_date: week[:end_date]), class: "card card-week clickable-week-card", style: "text-decoration: none; color: inherit;" do %> +
+

<%= week[:label] %>

+
šŸ“…
+
+
+
<%= week[:count] %>
+

applications

+
+ <% end %> + <% end %> +
- <% end %> - <% end %> +
+
diff --git a/app/views/opportunities/_form.html.erb b/app/views/opportunities/_form.html.erb index 8220490..0f09bd6 100644 --- a/app/views/opportunities/_form.html.erb +++ b/app/views/opportunities/_form.html.erb @@ -54,14 +54,9 @@ <%= form.label :status, style: "display: block" %> <%= form.select :status, options_for_select([ - ["Applied", "submitted"], - ["Under Review", "under_review"], - ["Phone Screen", "phone_screen"], - ["Interview Scheduled", "interview"], - ["Responded", "responded"], - ["Offer Received", "offer"], - ["Closed", "closed"], - ["Withdrawn", "withdrawn"] + ["Applied", "applied"], + ["Interviewing", "interviewing"], + ["Closed", "closed"] ], opportunity.status), { include_blank: "Select status" }, { style: "display: block; width: 250px; height: 30px;" } %> @@ -115,6 +110,16 @@
<%= render "product_manager_fields", opportunity: opportunity %>
+ + +
+ <%= render "support_engineer_fields", opportunity: opportunity %> +
+ + +
+ <%= render "other_fields", opportunity: opportunity %> +
diff --git a/app/views/opportunities/_opportunity.html.erb b/app/views/opportunities/_opportunity.html.erb index b0f8045..9b731b7 100644 --- a/app/views/opportunities/_opportunity.html.erb +++ b/app/views/opportunities/_opportunity.html.erb @@ -10,6 +10,10 @@ "#17a2b8" # Teal when "product_manager" "#6f42c1" # Purple + when "support_engineer" + "#fd7e14" # Orange + when "other" + "#343a40" # Dark else "#6c757d" # Default gray end @@ -22,7 +26,7 @@ font-size: 11px; white-space: nowrap; display: inline-block; - width: 80px; + width: 95px; text-align: center; "> <%= opportunity.role_type_label.gsub("Engineer", "Eng").gsub("Manager", "Mgr") %> diff --git a/app/views/opportunities/_opportunity_details.html.erb b/app/views/opportunities/_opportunity_details.html.erb index 515f402..66d0d14 100644 --- a/app/views/opportunities/_opportunity_details.html.erb +++ b/app/views/opportunities/_opportunity_details.html.erb @@ -4,7 +4,25 @@

Role Type: - + <% + badge_color = case opportunity.role_type + when "software_engineer" + "#6c757d" + when "sales_engineer" + "#28a745" + when "solutions_engineer" + "#17a2b8" + when "product_manager" + "#6f42c1" + when "support_engineer" + "#fd7e14" + when "other" + "#343a40" + else + "#6c757d" + end + %> + <%= opportunity.role_type_label %>

diff --git a/app/views/opportunities/_other_fields.html.erb b/app/views/opportunities/_other_fields.html.erb new file mode 100644 index 0000000..c7f553e --- /dev/null +++ b/app/views/opportunities/_other_fields.html.erb @@ -0,0 +1,5 @@ +
+

+ No role-specific fields for "Other". +

+
diff --git a/app/views/opportunities/_support_engineer_fields.html.erb b/app/views/opportunities/_support_engineer_fields.html.erb new file mode 100644 index 0000000..ab0972d --- /dev/null +++ b/app/views/opportunities/_support_engineer_fields.html.erb @@ -0,0 +1,5 @@ +
+

+ No additional fields for Support Engineer roles yet. +

+
diff --git a/app/views/opportunities/index.html.erb b/app/views/opportunities/index.html.erb index 8d97a3e..239acae 100644 --- a/app/views/opportunities/index.html.erb +++ b/app/views/opportunities/index.html.erb @@ -36,6 +36,18 @@ #{ params[:role_type] == 'product_manager' ? 'background-color: #6f42c1; color: white;' : 'background-color: white; color: #333; border: 1px solid #dee2e6;' }", onmouseover: "if (!this.style.backgroundColor.includes('rgb(111, 66, 193)')) { this.style.backgroundColor='#e9ecef'; }", onmouseout: "if (!this.style.backgroundColor.includes('rgb(111, 66, 193)')) { this.style.backgroundColor='white'; }" %> + + <%= link_to "Support Engineer", opportunities_path(role_type: "support_engineer"), + style: "padding: 6px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; transition: all 0.2s; width: 160px; text-align: center; + #{ params[:role_type] == 'support_engineer' ? 'background-color: #fd7e14; color: white;' : 'background-color: white; color: #333; border: 1px solid #dee2e6;' }", + onmouseover: "if (!this.style.backgroundColor.includes('rgb(253, 126, 20)')) { this.style.backgroundColor='#e9ecef'; }", + onmouseout: "if (!this.style.backgroundColor.includes('rgb(253, 126, 20)')) { this.style.backgroundColor='white'; }" %> + + <%= link_to "Other", opportunities_path(role_type: "other"), + style: "padding: 6px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; transition: all 0.2s; width: 160px; text-align: center; + #{ params[:role_type] == 'other' ? 'background-color: #343a40; color: white;' : 'background-color: white; color: #333; border: 1px solid #dee2e6;' }", + onmouseover: "if (!this.style.backgroundColor.includes('rgb(52, 58, 64)')) { this.style.backgroundColor='#e9ecef'; }", + onmouseout: "if (!this.style.backgroundColor.includes('rgb(52, 58, 64)')) { this.style.backgroundColor='white'; }" %>
<% if @role_filter %> diff --git a/db/migrate/20260203130000_normalize_opportunity_statuses.rb b/db/migrate/20260203130000_normalize_opportunity_statuses.rb new file mode 100644 index 0000000..0459664 --- /dev/null +++ b/db/migrate/20260203130000_normalize_opportunity_statuses.rb @@ -0,0 +1,21 @@ +class NormalizeOpportunityStatuses < ActiveRecord::Migration[8.0] + def up + execute <<~SQL + UPDATE opportunities + SET status = 'applied' + WHERE status = 'submitted'; + + UPDATE opportunities + SET status = 'interviewing' + WHERE status IN ('under_review', 'phone_screen', 'interview', 'responded', 'offer'); + + UPDATE opportunities + SET status = 'closed' + WHERE status = 'withdrawn'; + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 15acd6e..46892f6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_01_23_193352) do +ActiveRecord::Schema[8.0].define(version: 2026_02_03_130000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" diff --git a/lib/tasks/update_proptech_industry.rake b/lib/tasks/update_proptech_industry.rake new file mode 100644 index 0000000..ffad931 --- /dev/null +++ b/lib/tasks/update_proptech_industry.rake @@ -0,0 +1,36 @@ +namespace :companies do + desc "Update Real Estate related industries to PropTech" + task update_proptech_industry: :environment do + patterns = [ + "Real Estate", + "Real Estate Technology", + "Real Estate SaaS" + ] + + companies_to_update = Company.where( + patterns.map { |pattern| "industry ILIKE ?" }.join(" OR "), + *patterns.map { |pattern| "%#{pattern}%" } + ) + + count = companies_to_update.count + + if count.zero? + puts "No companies found with Real Estate related industries." + else + puts "Found #{count} #{'company'.pluralize(count)} to update:" + companies_to_update.each do |company| + puts " - #{company.name}: '#{company.industry}' -> 'PropTech'" + end + + print "\nProceed with update? (y/n): " + response = STDIN.gets.chomp.downcase + + if response == 'y' + updated = companies_to_update.update_all(industry: "PropTech") + puts "\nāœ“ Successfully updated #{updated} #{'company'.pluralize(updated)} to PropTech" + else + puts "\nāœ— Update cancelled" + end + end + end +end From acb9eb277b92d970c1d856108bd6374f5e7e08a6 Mon Sep 17 00:00:00 2001 From: ntxtthomas Date: Tue, 3 Feb 2026 22:15:44 -0600 Subject: [PATCH 2/4] updated rubocop --- .vscode/settings.json | 4 ++++ app/models/concerns/role_types/support_engineer.rb | 2 +- lib/tasks/update_proptech_industry.rake | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..969fb79 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "ruby.rubocop.useBundler": true, + "ruby.rubocop.commandPath": "bin/rubocop" +} diff --git a/app/models/concerns/role_types/support_engineer.rb b/app/models/concerns/role_types/support_engineer.rb index 8674df4..d53c423 100644 --- a/app/models/concerns/role_types/support_engineer.rb +++ b/app/models/concerns/role_types/support_engineer.rb @@ -26,7 +26,7 @@ def support_summary tier = role_metadata["support_tier"].upcase on_call = role_metadata["on_call"] ? "On-call" : nil - ["Tier #{tier}", on_call].compact.join(" • ") + [ "Tier #{tier}", on_call ].compact.join(" • ") end end end diff --git a/lib/tasks/update_proptech_industry.rake b/lib/tasks/update_proptech_industry.rake index ffad931..c0ae813 100644 --- a/lib/tasks/update_proptech_industry.rake +++ b/lib/tasks/update_proptech_industry.rake @@ -13,11 +13,11 @@ namespace :companies do ) count = companies_to_update.count - + if count.zero? puts "No companies found with Real Estate related industries." else - puts "Found #{count} #{'company'.pluralize(count)} to update:" + puts "Found #{count} #{"company".pluralize(count)} to update:" companies_to_update.each do |company| puts " - #{company.name}: '#{company.industry}' -> 'PropTech'" end @@ -25,9 +25,9 @@ namespace :companies do print "\nProceed with update? (y/n): " response = STDIN.gets.chomp.downcase - if response == 'y' + if response == "y" updated = companies_to_update.update_all(industry: "PropTech") - puts "\nāœ“ Successfully updated #{updated} #{'company'.pluralize(updated)} to PropTech" + puts "\nāœ“ Successfully updated #{updated} #{"company".pluralize(updated)} to PropTech" else puts "\nāœ— Update cancelled" end From 8aa50fb8d8eb1129e73275b5973604f1fdd08b7d Mon Sep 17 00:00:00 2001 From: ntxtthomas Date: Tue, 3 Feb 2026 22:19:37 -0600 Subject: [PATCH 3/4] update brakeman --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index de0f697..55ce6b9 100644 --- a/Gemfile +++ b/Gemfile @@ -51,7 +51,7 @@ group :development, :test do gem "pry-rails" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] - gem "brakeman", "~> 7.1.2", require: false + gem "brakeman", "~> 8.0.2", require: false # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 356bcbb..d429602 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.1.2) + brakeman (8.0.2) racc builder (3.3.0) capybara (3.40.0) @@ -378,7 +378,7 @@ PLATFORMS DEPENDENCIES bootsnap - brakeman (~> 7.1.2) + brakeman (~> 8.0.2) capybara csv debug From c2a221a2f7a714dbc3cd5c16c9c5bc95f7b0e0d8 Mon Sep 17 00:00:00 2001 From: ntxtthomas Date: Tue, 3 Feb 2026 22:24:26 -0600 Subject: [PATCH 4/4] update test --- test/system/opportunities_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/system/opportunities_test.rb b/test/system/opportunities_test.rb index b93ad6e..0bed448 100644 --- a/test/system/opportunities_test.rb +++ b/test/system/opportunities_test.rb @@ -36,7 +36,7 @@ class OpportunitiesTest < ApplicationSystemTestCase select @opportunity.company.name, from: "Company" fill_in "Notes", with: @opportunity.notes fill_in "Position title", with: @opportunity.position_title - select "Under Review", from: "Status" + select "Interviewing", from: "Status" check "Python" select "Indeed", from: "Source" click_on "Update Opportunity"