diff --git a/Gemfile b/Gemfile index 66bb164..f14786a 100644 --- a/Gemfile +++ b/Gemfile @@ -54,7 +54,7 @@ group :development, :test do gem "pry-rails" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] - gem "brakeman", "~> 8.0.2", require: false + gem "brakeman", "~> 8.0.4", require: false # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false @@ -72,4 +72,8 @@ group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem "capybara" gem "selenium-webdriver" + + # RSpec test helpers + gem "shoulda-matchers", "~> 6.0" + gem "factory_bot_rails" end diff --git a/Gemfile.lock b/Gemfile.lock index c4f0854..630a5d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (8.0.2) + brakeman (8.0.4) racc builder (3.3.0) capybara (3.40.0) @@ -111,6 +111,11 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) fugit (1.11.2) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -325,6 +330,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) + shoulda-matchers (6.5.0) + activesupport (>= 5.2.0) solid_cable (3.0.12) actioncable (>= 7.2) activejob (>= 7.2) @@ -396,10 +403,11 @@ PLATFORMS DEPENDENCIES bootsnap - brakeman (~> 8.0.2) + brakeman (~> 8.0.4) capybara csv debug + factory_bot_rails hirb importmap-rails jbuilder @@ -412,6 +420,7 @@ DEPENDENCIES rspec-rails rubocop-rails-omakase selenium-webdriver + shoulda-matchers (~> 6.0) solid_cable solid_cache solid_queue diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index 728e7cf..b953148 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -99,6 +99,11 @@ def set_company # Only allow a list of trusted parameters through. def company_params - params.expect(company: [ :name, :industry, :company_type, :location, :size, :website, :linkedin, :known_tech_stack ]) + params.expect(company: [ + :name, :industry, :company_type, :location, :size, :website, :linkedin, :known_tech_stack, + :primary_product, :revenue_model, :funding_stage, :estimated_revenue, :estimated_employees, + :growth_signal, :product_maturity, :engineering_maturity, :process_maturity, + :market_position, :competitor_tier, :brand_signal_strength, :market_size_estimate + ]) end end diff --git a/app/controllers/interview_sessions_controller.rb b/app/controllers/interview_sessions_controller.rb new file mode 100644 index 0000000..8f3f7f4 --- /dev/null +++ b/app/controllers/interview_sessions_controller.rb @@ -0,0 +1,53 @@ +class InterviewSessionsController < ApplicationController + before_action :set_interview_session, only: [ :show, :edit, :update, :destroy ] + + def index + @interview_sessions = InterviewSession.all.includes(:opportunity).order(date: :desc) + end + + def show + end + + def new + @interview_session = InterviewSession.new + end + + def create + @interview_session = InterviewSession.new(interview_session_params) + + if @interview_session.save + redirect_to @interview_session, notice: "Interview session was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + if @interview_session.update(interview_session_params) + redirect_to @interview_session, notice: "Interview session was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @interview_session.destroy + redirect_to interview_sessions_url, notice: "Interview session was successfully deleted." + end + + private + + def set_interview_session + @interview_session = InterviewSession.find(params[:id]) + end + + def interview_session_params + params.require(:interview_session).permit( + :opportunity_id, :stage, :date, :confidence_score, :clarity_score, + :questions_asked, :weak_areas, :strong_areas, :follow_up, :overall_signal + ) + end +end diff --git a/app/controllers/opportunities_controller.rb b/app/controllers/opportunities_controller.rb index f0a8c0e..3d395d0 100644 --- a/app/controllers/opportunities_controller.rb +++ b/app/controllers/opportunities_controller.rb @@ -25,7 +25,7 @@ def index sort_direction = params[:direction] == "desc" ? "desc" : "asc" # Validate sort column to prevent SQL injection - allowed_columns = %w[position_title application_date status remote tech_stack created_at salary_range chatgpt_match jobright_match linkedin_match role_type] + allowed_columns = %w[position_title application_date status remote tech_stack created_at salary_range bus_factor chatgpt_match jobright_match linkedin_match role_type] if allowed_columns.include?(sort_column) @opportunities = @opportunities.order("#{sort_column} #{sort_direction}") elsif sort_column == "company" @@ -111,7 +111,7 @@ def set_opportunity def opportunity_params params.expect(opportunity: [ :company_id, :position_title, :application_date, :status, :notes, :remote, - :tech_stack, :other_tech_stack, :source, :salary_range, :listing_url, + :tech_stack, :other_tech_stack, :source, :salary_range, :bus_factor, :listing_url, :chatgpt_match, :jobright_match, :linkedin_match, :role_type, technology_ids: [], role_metadata: {} diff --git a/app/controllers/star_stories_controller.rb b/app/controllers/star_stories_controller.rb new file mode 100644 index 0000000..31bcc94 --- /dev/null +++ b/app/controllers/star_stories_controller.rb @@ -0,0 +1,73 @@ +class StarStoriesController < ApplicationController + before_action :set_star_story, only: [ :show, :edit, :update, :destroy ] + + def index + @star_stories = StarStory.all.order(created_at: :desc) + + respond_to do |format| + format.html + format.csv do + exporter = StarStoriesCsvExporter.new(@star_stories) + send_data exporter.generate, + filename: "star_stories_#{Date.current.strftime('%Y-%m-%d')}.csv", + type: "text/csv" + end + end + end + + def new + @star_story = StarStory.new + end + + def create + @star_story = StarStory.new(star_story_params) + process_skills_from_form + + if @star_story.save + redirect_to @star_story, notice: "STAR story was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + process_skills_from_form + + if @star_story.update(star_story_params) + redirect_to @star_story, notice: "STAR story was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @star_story.destroy + redirect_to star_stories_url, notice: "STAR story was successfully deleted." + end + + def show + end + + private + + def set_star_story + @star_story = StarStory.find(params[:id]) + end + + def star_story_params + params.require(:star_story).permit( + :title, :situation, :task, :action, :result, :category, :outcome, + :strength_score, :notes, :skills, opportunity_ids: [] + ) + end + + def process_skills_from_form + if params[:star_story][:skills].is_a?(String) + skills_array = params[:star_story][:skills].split("\n").map(&:strip).reject(&:blank?) + params[:star_story][:skills] = skills_array + end + end +end diff --git a/app/helpers/interview_sessions_helper.rb b/app/helpers/interview_sessions_helper.rb new file mode 100644 index 0000000..f5b35af --- /dev/null +++ b/app/helpers/interview_sessions_helper.rb @@ -0,0 +1,2 @@ +module InterviewSessionsHelper +end diff --git a/app/helpers/star_stories_helper.rb b/app/helpers/star_stories_helper.rb new file mode 100644 index 0000000..77ddc5b --- /dev/null +++ b/app/helpers/star_stories_helper.rb @@ -0,0 +1,2 @@ +module StarStoriesHelper +end diff --git a/app/models/core_narrative.rb b/app/models/core_narrative.rb new file mode 100644 index 0000000..d1951cf --- /dev/null +++ b/app/models/core_narrative.rb @@ -0,0 +1,60 @@ +class CoreNarrative < ApplicationRecord + # Enums + enum :narrative_type, { + about_me: "about_me", + why_company: "why_company", + why_me: "why_me", + salary: "salary", + remote_positioning: "remote_positioning", + leadership: "leadership", + technical_depth: "technical_depth", + career_trajectory: "career_trajectory" + }, prefix: true + + enum :role_target, { + software_engineer: "software_engineer", + sales_engineer: "sales_engineer", + solutions_engineer: "solutions_engineer", + product_manager: "product_manager", + support_engineer: "support_engineer", + success_engineer: "success_engineer", + devops: "devops", + hybrid: "hybrid", + universal: "universal" + }, prefix: true + + # Validations + validates :narrative_type, presence: true + validates :content, presence: true + + # Callbacks + before_save :set_last_updated, if: :content_changed? + + # Scopes + scope :for_role, ->(role) { where(role_target: [ role, :universal ]) } + scope :by_type, ->(type) { where(narrative_type: type) } + scope :recently_updated, -> { where("last_updated >= ?", 30.days.ago).order(last_updated: :desc) } + scope :current_version, -> { where(version: "current").or(where(version: nil)) } + + # Methods + def word_count + content.to_s.split.size + end + + def estimated_reading_time + # Average reading speed: 200 words per minute + (word_count / 200.0).ceil + end + + def needs_update? + return true if last_updated.nil? + + last_updated < 60.days.ago + end + + private + + def set_last_updated + self.last_updated = Date.today + end +end diff --git a/app/models/interview_session.rb b/app/models/interview_session.rb new file mode 100644 index 0000000..9f61793 --- /dev/null +++ b/app/models/interview_session.rb @@ -0,0 +1,47 @@ +class InterviewSession < ApplicationRecord + belongs_to :opportunity + + # Enums + enum :stage, { + recruiter: "recruiter", + tech_screen: "tech_screen", + panel: "panel", + final: "final", + hiring_manager: "hiring_manager", + peer: "peer", + behavioral: "behavioral" + }, prefix: true + + enum :overall_signal, { + strong: "strong", + neutral: "neutral", + weak: "weak" + }, prefix: true + + # Validations + validates :stage, presence: true + validates :date, presence: true + validates :confidence_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true + validates :clarity_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true + + # Scopes + scope :recent, -> { order(date: :desc) } + scope :by_stage, ->(stage) { where(stage: stage) } + scope :strong_signals, -> { where(overall_signal: :strong) } + scope :this_month, -> { where(date: Date.today.beginning_of_month..Date.today.end_of_month) } + + # Methods + def performance_score + return nil unless confidence_score.present? && clarity_score.present? + + (confidence_score + clarity_score) / 2.0 + end + + def needs_follow_up? + follow_up.present? && follow_up.strip.length > 0 + end + + def has_preparation_gaps? + weak_areas.present? && weak_areas.strip.length > 0 + end +end diff --git a/app/models/opportunity.rb b/app/models/opportunity.rb index f5f15e1..484af75 100644 --- a/app/models/opportunity.rb +++ b/app/models/opportunity.rb @@ -11,6 +11,9 @@ class Opportunity < ApplicationRecord belongs_to :company has_many :opportunity_technologies, dependent: :destroy has_many :technologies, through: :opportunity_technologies + has_many :interview_sessions, dependent: :destroy + has_many :star_story_opportunities, dependent: :destroy + has_many :star_stories, through: :star_story_opportunities accepts_nested_attributes_for :opportunity_technologies, allow_destroy: true @@ -28,7 +31,30 @@ class Opportunity < ApplicationRecord "other" => "Other" }.freeze + # Career Intelligence Enums + enum :retirement_plan_type, { + four_oh_one_k: "401k", + simple_ira: "simple_ira", + none: "none", + unknown: "unknown" + }, prefix: :retirement + + enum :remote_type, { + remote: "remote", + hybrid: "hybrid", + onsite: "onsite" + }, prefix: :work + + enum :risk_level, { + low: "low", + medium: "medium", + high: "high" + }, prefix: :risk + validates :role_type, presence: true, inclusion: { in: ROLE_TYPES.keys } + validates :fit_score, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 10 }, allow_nil: true + validates :trajectory_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true + validates :strategic_value, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true before_save :shorten_urls before_save :standardize_salary_range diff --git a/app/models/star_story.rb b/app/models/star_story.rb new file mode 100644 index 0000000..c5eda0a --- /dev/null +++ b/app/models/star_story.rb @@ -0,0 +1,56 @@ +class StarStory < ApplicationRecord + # Associations + has_many :star_story_opportunities, dependent: :destroy + has_many :opportunities, through: :star_story_opportunities + + # Enums + enum :category, { + incident: "incident", + leadership: "leadership", + devops: "devops", + refactor: "refactor", + conflict: "conflict", + architecture: "architecture", + scaling: "scaling", + problem_solving: "problem_solving", + collaboration: "collaboration", + innovation: "innovation" + }, prefix: true + + enum :outcome, { + advanced: "advanced", + rejected: "rejected", + offer: "offer", + unknown: "unknown" + }, prefix: true + + # Validations + validates :title, presence: true + validates :strength_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true + validates :times_used, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + # Scopes + scope :top_rated, -> { where("strength_score >= ?", 4).order(strength_score: :desc) } + scope :frequently_used, -> { where("times_used > ?", 2).order(times_used: :desc) } + scope :by_category, ->(category) { where(category: category) } + scope :successful, -> { where(outcome: [ :advanced, :offer ]) } + + # Methods + def mark_as_used! + increment!(:times_used) + update(last_used_at: Date.today) + end + + def complete? + situation.present? && task.present? && action.present? && result.present? + end + + def incomplete_fields + fields = [] + fields << "situation" if situation.blank? + fields << "task" if task.blank? + fields << "action" if action.blank? + fields << "result" if result.blank? + fields + end +end diff --git a/app/models/star_story_opportunity.rb b/app/models/star_story_opportunity.rb new file mode 100644 index 0000000..ab1ece7 --- /dev/null +++ b/app/models/star_story_opportunity.rb @@ -0,0 +1,6 @@ +class StarStoryOpportunity < ApplicationRecord + belongs_to :star_story + belongs_to :opportunity + + validates :star_story_id, uniqueness: { scope: :opportunity_id } +end diff --git a/app/services/star_stories_csv_exporter.rb b/app/services/star_stories_csv_exporter.rb new file mode 100644 index 0000000..d84fec0 --- /dev/null +++ b/app/services/star_stories_csv_exporter.rb @@ -0,0 +1,43 @@ +require "csv" + +class StarStoriesCsvExporter + def initialize(star_stories) + @star_stories = star_stories + end + + def generate + CSV.generate(headers: true) do |csv| + csv << headers + + @star_stories.each do |story| + csv << [ + story.title, + story.situation, + story.task, + story.action, + story.result, + story.skills&.join(", "), + story.category&.titleize, + story.outcome&.titleize, + story.notes + ] + end + end + end + + private + + def headers + [ + "Title", + "Situation", + "Task", + "Action", + "Result", + "Skills", + "Category", + "Outcome", + "Notes" + ] + end +end diff --git a/app/views/companies/_company_details.html.erb b/app/views/companies/_company_details.html.erb index a77a04a..af3fa3d 100644 --- a/app/views/companies/_company_details.html.erb +++ b/app/views/companies/_company_details.html.erb @@ -40,6 +40,67 @@

<% end %> + + <% if company.primary_product.present? || company.revenue_model.present? || company.funding_stage.present? || company.product_maturity.present? %> +
+

Company Intelligence

+ + <% if company.primary_product.present? %> +

Primary Product: <%= company.primary_product %>

+ <% end %> + + <% if company.revenue_model.present? %> +

Revenue Model: <%= company.revenue_model %>

+ <% end %> + + <% if company.funding_stage.present? %> +

Funding Stage: <%= company.funding_stage %>

+ <% end %> + + <% if company.estimated_revenue.present? %> +

Estimated Revenue: <%= company.estimated_revenue %>

+ <% end %> + + <% if company.estimated_employees.present? %> +

Estimated Employees: <%= company.estimated_employees %>

+ <% end %> + + <% if company.growth_signal.present? %> +

Growth Signal: <%= company.growth_signal %>

+ <% end %> + +
+ <% if company.product_maturity.present? %> +

Product Maturity: <%= company.product_maturity %>/10

+ <% end %> + + <% if company.engineering_maturity.present? %> +

Engineering Maturity: <%= company.engineering_maturity %>/10

+ <% end %> + + <% if company.process_maturity.present? %> +

Process Maturity: <%= company.process_maturity %>/10

+ <% end %> +
+ + <% if company.market_position.present? %> +

Market Position: <%= company.market_position %>

+ <% end %> + + <% if company.competitor_tier.present? %> +

Competitor Tier: <%= company.competitor_tier %>

+ <% end %> + + <% if company.brand_signal_strength.present? %> +

Brand Signal Strength: <%= company.brand_signal_strength %>/10

+ <% end %> + + <% if company.market_size_estimate.present? %> +

Market Size: <%= company.market_size_estimate %>

+ <% end %> +
+ <% end %> + <% if company.tech_stack_summary.any? || company.known_tech_stack.present? %>

Technologies: diff --git a/app/views/companies/_form.html.erb b/app/views/companies/_form.html.erb index d926c2e..10381d2 100644 --- a/app/views/companies/_form.html.erb +++ b/app/views/companies/_form.html.erb @@ -79,6 +79,79 @@ <%= hidden_field_tag "company[known_tech_stack_list][]", "" %> - <%= form.submit style: "margin-top: 20px; background-color: #0d6efd; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + +

+

Company Intelligence

+ +
+ <%= form.label :primary_product, style: "display: block" %> + <%= form.text_area :primary_product, rows: 3, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :revenue_model, style: "display: block" %> + <%= form.select :revenue_model, [["Select...", ""], ["SaaS", "SaaS"], ["Enterprise License", "Enterprise License"], ["Freemium", "Freemium"], ["Marketplace", "Marketplace"], ["Advertising", "Advertising"], ["Consulting", "Consulting"]], {}, { style: "width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :funding_stage, style: "display: block" %> + <%= form.select :funding_stage, [["Select...", ""], ["Bootstrapped", "Bootstrapped"], ["Seed", "Seed"], ["Series A", "Series A"], ["Series B", "Series B"], ["Series C+", "Series C+"], ["Public", "Public"], ["Acquired", "Acquired"]], {}, { style: "width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :estimated_revenue, style: "display: block" %> + <%= form.text_field :estimated_revenue, placeholder: "e.g., $10M-$50M ARR", style: "width: 250px; height: 25px;" %> +
+ +
+ <%= form.label :estimated_employees, style: "display: block" %> + <%= form.number_field :estimated_employees, style: "width: 150px; height: 25px;" %> +
+ +
+ <%= form.label :growth_signal, style: "display: block" %> + <%= form.select :growth_signal, [["Select...", ""], ["Rapid Growth", "Rapid Growth"], ["Steady Growth", "Steady Growth"], ["Stable", "Stable"], ["Declining", "Declining"], ["Unknown", "Unknown"]], {}, { style: "width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :product_maturity, "Product Maturity (1-10)", style: "display: block" %> + <%= form.number_field :product_maturity, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + 1=early prototype, 10=mature/established +
+ +
+ <%= form.label :engineering_maturity, "Engineering Maturity (1-10)", style: "display: block" %> + <%= form.number_field :engineering_maturity, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + 1=chaos, 10=world-class practices +
+ +
+ <%= form.label :process_maturity, "Process Maturity (1-10)", style: "display: block" %> + <%= form.number_field :process_maturity, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + 1=ad-hoc, 10=optimized +
+ +
+ <%= form.label :market_position, style: "display: block" %> + <%= form.select :market_position, [["Select...", ""], ["Market Leader", "Market Leader"], ["Strong Challenger", "Strong Challenger"], ["Niche Player", "Niche Player"], ["Emerging", "Emerging"], ["Unknown", "Unknown"]], {}, { style: "width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :competitor_tier, style: "display: block" %> + <%= form.select :competitor_tier, [["Select...", ""], ["Tier 1 (FAANG-level)", "Tier 1"], ["Tier 2 (Unicorn/Strong)", "Tier 2"], ["Tier 3 (Mid-market)", "Tier 3"], ["Tier 4 (Small/Startup)", "Tier 4"]], {}, { style: "width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :brand_signal_strength, "Brand Signal Strength (1-10)", style: "display: block" %> + <%= form.number_field :brand_signal_strength, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + 1=unknown, 10=household name +
+ +
+ <%= form.label :market_size_estimate, style: "display: block" %> + <%= form.text_area :market_size_estimate, rows: 2, placeholder: "e.g., TAM $50B, growing 20% YoY", style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
<% end %> diff --git a/app/views/companies/index.html.erb b/app/views/companies/index.html.erb index 2adba42..085d278 100644 --- a/app/views/companies/index.html.erb +++ b/app/views/companies/index.html.erb @@ -21,7 +21,7 @@ <% end %> <% end %>
- <%= link_to "New company", new_company_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + <%= link_to "+", new_company_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block; font-size: 1.25rem; line-height: 1;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %>
diff --git a/app/views/contacts/index.html.erb b/app/views/contacts/index.html.erb index 8b24b0a..935c6ad 100644 --- a/app/views/contacts/index.html.erb +++ b/app/views/contacts/index.html.erb @@ -2,7 +2,10 @@ <% content_for :title, "Contacts" %> -

Contacts

+
+

Contacts

+ <%= link_to "+", new_contact_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block; font-size: 1.25rem; line-height: 1;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> +
@@ -22,5 +25,3 @@
- -<%= link_to "New contact", new_contact_path %> diff --git a/app/views/interview_sessions/_form.html.erb b/app/views/interview_sessions/_form.html.erb new file mode 100644 index 0000000..b80e680 --- /dev/null +++ b/app/views/interview_sessions/_form.html.erb @@ -0,0 +1,55 @@ +<%= form_with(model: interview_session, local: true) do |form| %> +
+ <%= form.label :opportunity_id, "Opportunity" %> + <%= form.collection_select :opportunity_id, Opportunity.all.order(:position_title), :id, :position_title, { prompt: "Select an opportunity" }, { class: "form-control" } %> +
+ +
+ <%= form.label :stage %> + <%= form.text_field :stage, class: "form-control", placeholder: "e.g., Phone Screen, Technical Interview, Final Round" %> +
+ +
+ <%= form.label :date %> + <%= form.date_field :date, class: "form-control" %> +
+ +
+ <%= form.label :confidence_score, "Confidence Score (1-10)" %> + <%= form.number_field :confidence_score, class: "form-control", min: 1, max: 10 %> +
+ +
+ <%= form.label :clarity_score, "Clarity Score (1-10)" %> + <%= form.number_field :clarity_score, class: "form-control", min: 1, max: 10 %> +
+ +
+ <%= form.label :questions_asked %> + <%= form.text_area :questions_asked, class: "form-control", rows: 4, placeholder: "What questions were you asked?" %> +
+ +
+ <%= form.label :weak_areas %> + <%= form.text_area :weak_areas, class: "form-control", rows: 3, placeholder: "Areas where you struggled or could improve" %> +
+ +
+ <%= form.label :strong_areas %> + <%= form.text_area :strong_areas, class: "form-control", rows: 3, placeholder: "Areas where you performed well" %> +
+ +
+ <%= form.label :follow_up %> + <%= form.text_area :follow_up, class: "form-control", rows: 3, placeholder: "Follow-up actions or notes" %> +
+ +
+ <%= form.label :overall_signal %> + <%= form.text_area :overall_signal, class: "form-control", rows: 3, placeholder: "Overall assessment of how the interview went" %> +
+ +
+ <%= form.submit class: "btn btn-primary" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/interview_sessions/edit.html.erb b/app/views/interview_sessions/edit.html.erb new file mode 100644 index 0000000..9dc1b1a --- /dev/null +++ b/app/views/interview_sessions/edit.html.erb @@ -0,0 +1,4 @@ +

InterviewSessions#edit

+ +<%= render 'form', interview_session: @interview_session %> +<%= link_to "Back", interview_session_path(@interview_session) %> \ No newline at end of file diff --git a/app/views/interview_sessions/index.html.erb b/app/views/interview_sessions/index.html.erb new file mode 100644 index 0000000..f452cec --- /dev/null +++ b/app/views/interview_sessions/index.html.erb @@ -0,0 +1,2 @@ +

InterviewSessions#index

+

Find me in app/views/interview_sessions/index.html.erb

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

InterviewSessions#new

+ +<%= render 'form', interview_session: @interview_session %> + +<%= link_to "Back", interview_sessions_path %> \ No newline at end of file diff --git a/app/views/interview_sessions/show.html.erb b/app/views/interview_sessions/show.html.erb new file mode 100644 index 0000000..8bbcd6c --- /dev/null +++ b/app/views/interview_sessions/show.html.erb @@ -0,0 +1,2 @@ +

InterviewSessions#show

+

Find me in app/views/interview_sessions/show.html.erb

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9968bde..0a20601 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -30,6 +30,8 @@ <%= link_to "Companies", companies_path, class: "nav-link" %> <%= link_to "Contacts", contacts_path, class: "nav-link" %> <%= link_to "Opportunities", opportunities_path, class: "nav-link" %> + <%= link_to "STAR", star_stories_path, class: "nav-link" %> + <%= link_to "Interviews", interview_sessions_path, class: "nav-link" %> diff --git a/app/views/opportunities/_form.html.erb b/app/views/opportunities/_form.html.erb index 3972493..9e402ce 100644 --- a/app/views/opportunities/_form.html.erb +++ b/app/views/opportunities/_form.html.erb @@ -160,6 +160,12 @@ <%= form.text_field :salary_range, style: "width: 250px; height: 25px;" %> +
+ <%= form.label :bus_factor, "Bus Factor (1-10)", style: "display: block" %> + <%= form.number_field :bus_factor, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + How many key people leaving would collapse the project/company? (1=very fragile, 10=very stable) +
+
<%= form.label :listing_url, "Listing URL", style: "display: block" %> <%= form.url_field :listing_url, placeholder: "https://...", style: "width: 250px; height: 25px;" %> diff --git a/app/views/opportunities/_opportunity.html.erb b/app/views/opportunities/_opportunity.html.erb index a55e7c5..dce5f85 100644 --- a/app/views/opportunities/_opportunity.html.erb +++ b/app/views/opportunities/_opportunity.html.erb @@ -83,6 +83,7 @@ <% end %> <%= opportunity.salary_range %> + <%= opportunity.bus_factor %> <%= opportunity.chatgpt_match %> <%= opportunity.jobright_match %> <%= opportunity.linkedin_match %> diff --git a/app/views/opportunities/index.html.erb b/app/views/opportunities/index.html.erb index 2da0c1b..3c16a47 100644 --- a/app/views/opportunities/index.html.erb +++ b/app/views/opportunities/index.html.erb @@ -32,7 +32,7 @@ <% end %> <% end %>
- <%= link_to "New opportunity", new_opportunity_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + <%= link_to "+", new_opportunity_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block; font-size: 1.25rem; line-height: 1;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> <%= link_to "Download CSV", opportunities_path(format: :csv, start_date: params[:start_date], end_date: params[:end_date]), style: "padding: 8px 16px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#5a6268'", onmouseout: "this.style.backgroundColor='#6c757d'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %>
@@ -50,6 +50,7 @@ <%= sortable_link :source %> <%= sortable_link :tech_stack, "Tech Stack" %> <%= sortable_link :salary_range, "Salary Range" %> + <%= sortable_link :bus_factor, "Bus Factor" %> <%= sortable_link :chatgpt_match, "ChatGPT" %> <%= sortable_link :jobright_match, "Jobright" %> <%= sortable_link :linkedin_match, "LinkedIn" %> diff --git a/app/views/star_stories/_form.html.erb b/app/views/star_stories/_form.html.erb new file mode 100644 index 0000000..66260d3 --- /dev/null +++ b/app/views/star_stories/_form.html.erb @@ -0,0 +1,74 @@ +<%= form_with(model: star_story) do |form| %> + <% if star_story.errors.any? %> +
+

<%= pluralize(star_story.errors.count, "error") %> prohibited this STAR story from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :title, style: "display: block" %> + <%= form.text_field :title, style: "width: 500px; height: 25px;" %> +
+ +
+ <%= form.label :situation, style: "display: block" %> + <%= form.text_area :situation, rows: 4, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :task, style: "display: block" %> + <%= form.text_area :task, rows: 4, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :action, style: "display: block" %> + <%= form.text_area :action, rows: 4, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :result, style: "display: block" %> + <%= form.text_area :result, rows: 4, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :skills, "Skills (one per line)", style: "display: block" %> + <%= form.text_area :skills, value: star_story.skills&.join("\n"), rows: 3, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> + Enter each skill on a new line +
+ +
+ <%= form.label :category, style: "display: block" %> + <%= form.select :category, StarStory.categories.keys.map { |k| [k.titleize, k] }, { prompt: "Select a category" }, { style: "display: block; width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :outcome, style: "display: block" %> + <%= form.select :outcome, StarStory.outcomes.keys.map { |k| [k.titleize, k] }, { prompt: "Select an outcome" }, { style: "display: block; width: 250px; height: 30px;" } %> +
+ +
+ <%= form.label :strength_score, "Strength Score (1-10)", style: "display: block" %> + <%= form.number_field :strength_score, min: 1, max: 10, style: "width: 100px; height: 25px;" %> + How compelling/impactful is this story? (1=weak, 10=very strong) +
+ +
+ <%= form.label :notes, "Notes/Reflection", style: "display: block" %> + <%= form.text_area :notes, rows: 4, style: "width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" %> +
+ +
+ <%= form.label :opportunity_ids, "Related Opportunities", style: "display: block" %> + <%= form.collection_select :opportunity_ids, Opportunity.all.order(:position_title), :id, :position_title, {}, { multiple: true, style: "display: block; width: 500px; height: 150px;" } %> +
+ +
+ <%= form.submit %> +
+<% end %> \ No newline at end of file diff --git a/app/views/star_stories/edit.html.erb b/app/views/star_stories/edit.html.erb new file mode 100644 index 0000000..b256ee8 --- /dev/null +++ b/app/views/star_stories/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Edit STAR Story" %> + +

Edit STAR Story

+ +<%= render 'form', star_story: @star_story %> + +
+ +
+ <%= link_to "Show this STAR story", @star_story %> | + <%= link_to "Back to STAR stories", star_stories_path %> +
\ No newline at end of file diff --git a/app/views/star_stories/index.html.erb b/app/views/star_stories/index.html.erb new file mode 100644 index 0000000..628e100 --- /dev/null +++ b/app/views/star_stories/index.html.erb @@ -0,0 +1,48 @@ +

<%= notice %>

+ +<% content_for :title, "STAR Stories" %> + +
+
+

STAR Stories

+
+ <%= link_to "+", new_star_story_path, style: "padding: 8px 16px; background-color: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block; font-size: 1.25rem; line-height: 1;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + <%= link_to "Download", star_stories_path(format: :csv), style: "padding: 8px 16px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#5a6268'", onmouseout: "this.style.backgroundColor='#6c757d'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> +
+
+
+ +
+ + + + + + + + + + + + + + + + <% @star_stories.each do |star_story| %> + + + + + + + + + + + + <% end %> + +
TitleSituationTaskActionResultSkillsCategoryOutcomeNotes
<%= star_story.title %><%= truncate(star_story.situation, length: 100) %><%= truncate(star_story.task, length: 100) %><%= truncate(star_story.action, length: 100) %><%= truncate(star_story.result, length: 100) %><%= star_story.skills&.join(", ") %><%= star_story.category&.titleize %><%= star_story.outcome&.titleize %><%= truncate(star_story.notes, length: 100) %>
+
+ + diff --git a/app/views/star_stories/new.html.erb b/app/views/star_stories/new.html.erb new file mode 100644 index 0000000..b924e47 --- /dev/null +++ b/app/views/star_stories/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New STAR Story" %> + +

New STAR Story

+ +<%= render 'form', star_story: @star_story %> + +
+ +
+ <%= link_to "Back to STAR stories", star_stories_path %> +
\ No newline at end of file diff --git a/app/views/star_stories/show.html.erb b/app/views/star_stories/show.html.erb new file mode 100644 index 0000000..f69f7a8 --- /dev/null +++ b/app/views/star_stories/show.html.erb @@ -0,0 +1,66 @@ +

<%= notice %>

+ +<% content_for :title, @star_story.title %> + +

<%= @star_story.title %>

+ +
+
+ Category: <%= @star_story.category&.titleize %> | + Outcome: <%= @star_story.outcome&.titleize %> | + Strength: <%= @star_story.strength_score %>/10 +
+ +
+

Situation

+

<%= @star_story.situation %>

+
+ +
+

Task

+

<%= @star_story.task %>

+
+ +
+

Action

+

<%= @star_story.action %>

+
+ +
+

Result

+

<%= @star_story.result %>

+
+ + <% if @star_story.skills&.any? %> +
+ Skills: + <% @star_story.skills.each do |skill| %> + <%= skill %> + <% end %> +
+ <% end %> + + <% if @star_story.notes.present? %> +
+

Notes

+

<%= @star_story.notes %>

+
+ <% end %> + + <% if @star_story.opportunities.any? %> +
+

Related Opportunities

+ +
+ <% end %> +
+ +
+ <%= link_to "Edit this STAR story", edit_star_story_path(@star_story), style: "background-color: #0d6efd; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#0b5ed7'", onmouseout: "this.style.backgroundColor='#0d6efd'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + <%= link_to "Back to STAR stories", star_stories_path, style: "background-color: #6c757d; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block;", onmouseover: "this.style.backgroundColor='#5a6268'", onmouseout: "this.style.backgroundColor='#6c757d'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> + <%= button_to "Destroy this STAR story", @star_story, method: :delete, style: "margin: 0; background-color: #dc3545; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease;", data: { confirm: "Are you sure?" }, onmouseover: "this.style.backgroundColor='#c82333'", onmouseout: "this.style.backgroundColor='#dc3545'", onmousedown: "this.style.transform='scale(0.95)'", onmouseup: "this.style.transform='scale(1)'" %> +
diff --git a/config/routes.rb b/config/routes.rb index 846762d..ea6ca24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,11 @@ Rails.application.routes.draw do - get "dashboard/index" get "dashboard" => "dashboard#index" resources :opportunities resources :interactions + resources :interview_sessions resources :contacts resources :companies + resources :star_stories # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20260227180045_create_star_stories.rb b/db/migrate/20260227180045_create_star_stories.rb new file mode 100644 index 0000000..1fa0a32 --- /dev/null +++ b/db/migrate/20260227180045_create_star_stories.rb @@ -0,0 +1,24 @@ +class CreateStarStories < ActiveRecord::Migration[8.0] + def change + create_table :star_stories do |t| + t.string :title, null: false + t.text :situation + t.text :task + t.text :action + t.text :result + t.string :skills, array: true, default: [] + t.string :category + t.integer :strength_score + t.integer :times_used, default: 0 + t.date :last_used_at + t.string :outcome + t.text :notes + + t.timestamps + end + + add_index :star_stories, :category + add_index :star_stories, :skills, using: :gin + add_index :star_stories, :strength_score + end +end diff --git a/db/migrate/20260227180111_create_star_story_opportunities.rb b/db/migrate/20260227180111_create_star_story_opportunities.rb new file mode 100644 index 0000000..7a25916 --- /dev/null +++ b/db/migrate/20260227180111_create_star_story_opportunities.rb @@ -0,0 +1,10 @@ +class CreateStarStoryOpportunities < ActiveRecord::Migration[8.0] + def change + create_table :star_story_opportunities do |t| + t.references :star_story, null: false, foreign_key: true + t.references :opportunity, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20260227180213_create_interview_sessions.rb b/db/migrate/20260227180213_create_interview_sessions.rb new file mode 100644 index 0000000..9f6fb27 --- /dev/null +++ b/db/migrate/20260227180213_create_interview_sessions.rb @@ -0,0 +1,22 @@ +class CreateInterviewSessions < ActiveRecord::Migration[8.0] + def change + create_table :interview_sessions do |t| + t.references :opportunity, null: false, foreign_key: true + t.string :stage, null: false + t.date :date, null: false + t.integer :confidence_score + t.integer :clarity_score + t.text :questions_asked + t.text :weak_areas + t.text :strong_areas + t.text :follow_up + t.string :overall_signal + + t.timestamps + end + + add_index :interview_sessions, [ :opportunity_id, :date ] + add_index :interview_sessions, :stage + add_index :interview_sessions, :overall_signal + end +end diff --git a/db/migrate/20260227180241_create_core_narratives.rb b/db/migrate/20260227180241_create_core_narratives.rb new file mode 100644 index 0000000..61a768e --- /dev/null +++ b/db/migrate/20260227180241_create_core_narratives.rb @@ -0,0 +1,16 @@ +class CreateCoreNarratives < ActiveRecord::Migration[8.0] + def change + create_table :core_narratives do |t| + t.string :narrative_type, null: false + t.string :version + t.string :role_target + t.text :content, null: false + t.date :last_updated + + t.timestamps + end + + add_index :core_narratives, [ :narrative_type, :role_target ] + add_index :core_narratives, :narrative_type + end +end diff --git a/db/migrate/20260227180326_add_career_intel_to_opportunities.rb b/db/migrate/20260227180326_add_career_intel_to_opportunities.rb new file mode 100644 index 0000000..e1334bf --- /dev/null +++ b/db/migrate/20260227180326_add_career_intel_to_opportunities.rb @@ -0,0 +1,17 @@ +class AddCareerIntelToOpportunities < ActiveRecord::Migration[8.0] + def change + add_column :opportunities, :fit_score, :decimal, precision: 3, scale: 2 + add_column :opportunities, :income_range_low, :integer + add_column :opportunities, :income_range_high, :integer + add_column :opportunities, :retirement_plan_type, :string + add_column :opportunities, :remote_type, :string + add_column :opportunities, :company_size, :integer + add_column :opportunities, :risk_level, :string + add_column :opportunities, :trajectory_score, :integer + add_column :opportunities, :strategic_value, :integer + + add_index :opportunities, :fit_score + add_index :opportunities, :risk_level + add_index :opportunities, :remote_type + end +end diff --git a/db/migrate/20260227180347_add_intelligence_to_companies.rb b/db/migrate/20260227180347_add_intelligence_to_companies.rb new file mode 100644 index 0000000..55d69d5 --- /dev/null +++ b/db/migrate/20260227180347_add_intelligence_to_companies.rb @@ -0,0 +1,21 @@ +class AddIntelligenceToCompanies < ActiveRecord::Migration[8.0] + def change + # industry already exists, skip it + add_column :companies, :primary_product, :text + add_column :companies, :revenue_model, :string + add_column :companies, :funding_stage, :string + add_column :companies, :estimated_revenue, :string + add_column :companies, :estimated_employees, :integer + add_column :companies, :growth_signal, :string + add_column :companies, :product_maturity, :integer + add_column :companies, :engineering_maturity, :integer + add_column :companies, :process_maturity, :integer + add_column :companies, :market_position, :string + add_column :companies, :competitor_tier, :string + add_column :companies, :brand_signal_strength, :integer + + add_index :companies, :industry + add_index :companies, :funding_stage + add_index :companies, :growth_signal + end +end diff --git a/db/migrate/20260227180400_add_market_intel_to_companies.rb b/db/migrate/20260227180400_add_market_intel_to_companies.rb new file mode 100644 index 0000000..2c24d1d --- /dev/null +++ b/db/migrate/20260227180400_add_market_intel_to_companies.rb @@ -0,0 +1,12 @@ +class AddMarketIntelToCompanies < ActiveRecord::Migration[8.0] + def change + add_column :companies, :market_size_estimate, :text + add_column :companies, :market_growth_rate, :string + add_column :companies, :regulatory_environment, :string + add_column :companies, :switching_costs, :string + add_column :companies, :industry_volatility, :integer + add_column :companies, :tech_disruption_risk, :integer + add_column :companies, :personal_upside_score, :integer + add_column :companies, :career_risk_score, :integer + end +end diff --git a/db/migrate/20260228015924_add_bus_factor_to_opportunities.rb b/db/migrate/20260228015924_add_bus_factor_to_opportunities.rb new file mode 100644 index 0000000..2093913 --- /dev/null +++ b/db/migrate/20260228015924_add_bus_factor_to_opportunities.rb @@ -0,0 +1,5 @@ +class AddBusFactorToOpportunities < ActiveRecord::Migration[8.0] + def change + add_column :opportunities, :bus_factor, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 46892f6..9358bf7 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_02_03_130000) do +ActiveRecord::Schema[8.0].define(version: 2026_02_28_015924) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -25,6 +25,29 @@ t.text "known_tech_stack" t.string "company_type" t.string "size" + t.text "primary_product" + t.string "revenue_model" + t.string "funding_stage" + t.string "estimated_revenue" + t.integer "estimated_employees" + t.string "growth_signal" + t.integer "product_maturity" + t.integer "engineering_maturity" + t.integer "process_maturity" + t.string "market_position" + t.string "competitor_tier" + t.integer "brand_signal_strength" + t.text "market_size_estimate" + t.string "market_growth_rate" + t.string "regulatory_environment" + t.string "switching_costs" + t.integer "industry_volatility" + t.integer "tech_disruption_risk" + t.integer "personal_upside_score" + t.integer "career_risk_score" + t.index ["funding_stage"], name: "index_companies_on_funding_stage" + t.index ["growth_signal"], name: "index_companies_on_growth_signal" + t.index ["industry"], name: "index_companies_on_industry" end create_table "contacts", force: :cascade do |t| @@ -41,6 +64,18 @@ t.index ["company_id"], name: "index_contacts_on_company_id" end + create_table "core_narratives", force: :cascade do |t| + t.string "narrative_type", null: false + t.string "version" + t.string "role_target" + t.text "content", null: false + t.date "last_updated" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["narrative_type", "role_target"], name: "index_core_narratives_on_narrative_type_and_role_target" + t.index ["narrative_type"], name: "index_core_narratives_on_narrative_type" + end + create_table "interactions", force: :cascade do |t| t.bigint "contact_id" t.bigint "company_id", null: false @@ -54,6 +89,25 @@ t.index ["contact_id"], name: "index_interactions_on_contact_id" end + create_table "interview_sessions", force: :cascade do |t| + t.bigint "opportunity_id", null: false + t.string "stage", null: false + t.date "date", null: false + t.integer "confidence_score" + t.integer "clarity_score" + t.text "questions_asked" + t.text "weak_areas" + t.text "strong_areas" + t.text "follow_up" + t.string "overall_signal" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["opportunity_id", "date"], name: "index_interview_sessions_on_opportunity_id_and_date" + t.index ["opportunity_id"], name: "index_interview_sessions_on_opportunity_id" + t.index ["overall_signal"], name: "index_interview_sessions_on_overall_signal" + t.index ["stage"], name: "index_interview_sessions_on_stage" + end + create_table "opportunities", force: :cascade do |t| t.bigint "company_id", null: false t.string "position_title" @@ -73,7 +127,20 @@ t.text "other_tech_stack" t.string "role_type", default: "software_engineer", null: false t.jsonb "role_metadata", default: {}, null: false + t.decimal "fit_score", precision: 3, scale: 2 + t.integer "income_range_low" + t.integer "income_range_high" + t.string "retirement_plan_type" + t.string "remote_type" + t.integer "company_size" + t.string "risk_level" + t.integer "trajectory_score" + t.integer "strategic_value" + t.integer "bus_factor" t.index ["company_id"], name: "index_opportunities_on_company_id" + t.index ["fit_score"], name: "index_opportunities_on_fit_score" + t.index ["remote_type"], name: "index_opportunities_on_remote_type" + t.index ["risk_level"], name: "index_opportunities_on_risk_level" t.index ["role_metadata"], name: "index_opportunities_on_role_metadata", using: :gin t.index ["role_type"], name: "index_opportunities_on_role_type" end @@ -88,6 +155,35 @@ t.index ["technology_id"], name: "index_opportunity_technologies_on_technology_id" end + create_table "star_stories", force: :cascade do |t| + t.string "title", null: false + t.text "situation" + t.text "task" + t.text "action" + t.text "result" + t.string "skills", default: [], array: true + t.string "category" + t.integer "strength_score" + t.integer "times_used", default: 0 + t.date "last_used_at" + t.string "outcome" + t.text "notes" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["category"], name: "index_star_stories_on_category" + t.index ["skills"], name: "index_star_stories_on_skills", using: :gin + t.index ["strength_score"], name: "index_star_stories_on_strength_score" + end + + create_table "star_story_opportunities", force: :cascade do |t| + t.bigint "star_story_id", null: false + t.bigint "opportunity_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["opportunity_id"], name: "index_star_story_opportunities_on_opportunity_id" + t.index ["star_story_id"], name: "index_star_story_opportunities_on_star_story_id" + end + create_table "technologies", force: :cascade do |t| t.string "name", null: false t.string "category", null: false @@ -100,7 +196,10 @@ add_foreign_key "contacts", "companies" add_foreign_key "interactions", "companies" add_foreign_key "interactions", "contacts" + add_foreign_key "interview_sessions", "opportunities" add_foreign_key "opportunities", "companies" add_foreign_key "opportunity_technologies", "opportunities" add_foreign_key "opportunity_technologies", "technologies" + add_foreign_key "star_story_opportunities", "opportunities" + add_foreign_key "star_story_opportunities", "star_stories" end diff --git a/spec/models/star_story_spec.rb b/spec/models/star_story_spec.rb new file mode 100644 index 0000000..044deb2 --- /dev/null +++ b/spec/models/star_story_spec.rb @@ -0,0 +1,113 @@ +require 'rails_helper' + +RSpec.describe StarStory, type: :model do + describe 'associations' do + it { should have_many(:star_story_opportunities).dependent(:destroy) } + it { should have_many(:opportunities).through(:star_story_opportunities) } + end + + describe 'validations' do + it { should validate_presence_of(:title) } + + it 'validates strength_score is between 1 and 10' do + story = StarStory.new(title: "Test", strength_score: 11) + expect(story).not_to be_valid + expect(story.errors[:strength_score]).to include("must be less than or equal to 10") + end + + it 'validates times_used is non-negative' do + story = StarStory.new(title: "Test", times_used: -1) + expect(story).not_to be_valid + end + end + + describe 'enums' do + it 'defines category enum' do + expect(StarStory.categories.keys).to include('incident', 'leadership', 'devops') + end + + it 'defines outcome enum' do + expect(StarStory.outcomes.keys).to include('advanced', 'rejected', 'offer', 'unknown') + end + end + + describe 'scopes' do + let!(:top_story) { StarStory.create!(title: "Top Story", strength_score: 5) } + let!(:weak_story) { StarStory.create!(title: "Weak Story", strength_score: 2) } + let!(:frequent_story) { StarStory.create!(title: "Frequent", times_used: 5) } + let!(:rare_story) { StarStory.create!(title: "Rare", times_used: 1) } + + describe '.top_rated' do + it 'returns stories with strength_score >= 4' do + expect(StarStory.top_rated).to include(top_story) + expect(StarStory.top_rated).not_to include(weak_story) + end + end + + describe '.frequently_used' do + it 'returns stories used more than 2 times' do + expect(StarStory.frequently_used).to include(frequent_story) + expect(StarStory.frequently_used).not_to include(rare_story) + end + end + + describe '.successful' do + let!(:offer_story) { StarStory.create!(title: "Got Offer", outcome: :offer) } + let!(:rejected_story) { StarStory.create!(title: "Rejected", outcome: :rejected) } + + it 'returns stories with positive outcomes' do + expect(StarStory.successful).to include(offer_story) + expect(StarStory.successful).not_to include(rejected_story) + end + end + end + + describe '#mark_as_used!' do + let(:story) { StarStory.create!(title: "Test Story", times_used: 0) } + + it 'increments times_used' do + expect { story.mark_as_used! }.to change { story.times_used }.by(1) + end + + it 'updates last_used_at to today' do + story.mark_as_used! + expect(story.last_used_at).to eq(Date.today) + end + end + + describe '#complete?' do + it 'returns true when all STAR fields are present' do + story = StarStory.new( + title: "Complete", + situation: "S", + task: "T", + action: "A", + result: "R" + ) + expect(story.complete?).to be true + end + + it 'returns false when any STAR field is missing' do + story = StarStory.new(title: "Incomplete", situation: "S", task: "T") + expect(story.complete?).to be false + end + end + + describe '#incomplete_fields' do + it 'returns array of missing STAR fields' do + story = StarStory.new(title: "Test", situation: "S", task: "T") + expect(story.incomplete_fields).to contain_exactly("action", "result") + end + + it 'returns empty array when complete' do + story = StarStory.new( + title: "Complete", + situation: "S", + task: "T", + action: "A", + result: "R" + ) + expect(story.incomplete_fields).to be_empty + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ef75d46..61018d3 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -70,3 +70,11 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") end + +# Shoulda Matchers configuration +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/spec/views/interview_sessions/edit.html.erb_spec.rb b/spec/views/interview_sessions/edit.html.erb_spec.rb new file mode 100644 index 0000000..2e03ff2 --- /dev/null +++ b/spec/views/interview_sessions/edit.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "interview_sessions/edit.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/interview_sessions/index.html.erb_spec.rb b/spec/views/interview_sessions/index.html.erb_spec.rb new file mode 100644 index 0000000..c0f405e --- /dev/null +++ b/spec/views/interview_sessions/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "interview_sessions/index.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/interview_sessions/new.html.erb_spec.rb b/spec/views/interview_sessions/new.html.erb_spec.rb new file mode 100644 index 0000000..898ab92 --- /dev/null +++ b/spec/views/interview_sessions/new.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "interview_sessions/new.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/interview_sessions/show.html.erb_spec.rb b/spec/views/interview_sessions/show.html.erb_spec.rb new file mode 100644 index 0000000..b32d795 --- /dev/null +++ b/spec/views/interview_sessions/show.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "interview_sessions/show.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/star_stories/edit.html.erb_spec.rb b/spec/views/star_stories/edit.html.erb_spec.rb new file mode 100644 index 0000000..5102329 --- /dev/null +++ b/spec/views/star_stories/edit.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "star_stories/edit.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/star_stories/index.html.erb_spec.rb b/spec/views/star_stories/index.html.erb_spec.rb new file mode 100644 index 0000000..d2f906c --- /dev/null +++ b/spec/views/star_stories/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "star_stories/index.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/star_stories/new.html.erb_spec.rb b/spec/views/star_stories/new.html.erb_spec.rb new file mode 100644 index 0000000..9e4c63b --- /dev/null +++ b/spec/views/star_stories/new.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "star_stories/new.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/star_stories/show.html.erb_spec.rb b/spec/views/star_stories/show.html.erb_spec.rb new file mode 100644 index 0000000..b1bb2f9 --- /dev/null +++ b/spec/views/star_stories/show.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "star_stories/show.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/test/controllers/dashboard_controller_test.rb b/test/controllers/dashboard_controller_test.rb index 447c045..a46443c 100644 --- a/test/controllers/dashboard_controller_test.rb +++ b/test/controllers/dashboard_controller_test.rb @@ -2,7 +2,7 @@ class DashboardControllerTest < ActionDispatch::IntegrationTest test "should get index" do - get dashboard_index_url + get dashboard_url assert_response :success end end