From 5968ed74d59f1e133b33edd8b69731de559f61fd Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 4 Dec 2025 03:02:13 -0800 Subject: [PATCH] add joined_at to memberships and phantoms for historical data - editable field separate from created_at - active_during scope uses joined_at for filtering - backfills from created_at in migration --- .../api/v1/crew_membership_blueprint.rb | 6 +++--- .../api/v1/phantom_player_blueprint.rb | 2 +- app/models/crew_membership.rb | 13 ++++++++++++ app/models/phantom_player.rb | 20 ++++++++++++++++++ ...to_crew_memberships_and_phantom_players.rb | 21 +++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20251204102935_add_joined_at_to_crew_memberships_and_phantom_players.rb diff --git a/app/blueprints/api/v1/crew_membership_blueprint.rb b/app/blueprints/api/v1/crew_membership_blueprint.rb index f72fa5c..e72103b 100644 --- a/app/blueprints/api/v1/crew_membership_blueprint.rb +++ b/app/blueprints/api/v1/crew_membership_blueprint.rb @@ -3,10 +3,10 @@ module Api module V1 class CrewMembershipBlueprint < ApiBlueprint - fields :role, :retired, :retired_at, :created_at + fields :role, :retired, :retired_at, :joined_at, :created_at view :with_user do - fields :role, :retired, :retired_at, :created_at + fields :role, :retired, :retired_at, :joined_at, :created_at field :user do |membership| UserBlueprint.render_as_hash(membership.user, view: :minimal) @@ -14,7 +14,7 @@ module Api end view :with_crew do - fields :role, :retired, :retired_at, :created_at + fields :role, :retired, :retired_at, :joined_at, :created_at field :crew do |membership| CrewBlueprint.render_as_hash(membership.crew, view: :minimal) diff --git a/app/blueprints/api/v1/phantom_player_blueprint.rb b/app/blueprints/api/v1/phantom_player_blueprint.rb index 19a7432..4cfc529 100644 --- a/app/blueprints/api/v1/phantom_player_blueprint.rb +++ b/app/blueprints/api/v1/phantom_player_blueprint.rb @@ -3,7 +3,7 @@ module Api module V1 class PhantomPlayerBlueprint < ApiBlueprint - fields :name, :granblue_id, :notes, :claim_confirmed + fields :name, :granblue_id, :notes, :claim_confirmed, :retired, :retired_at, :joined_at field :claimed do |phantom| phantom.claimed_by_id.present? diff --git a/app/models/crew_membership.rb b/app/models/crew_membership.rb index cc0b749..ef06eef 100644 --- a/app/models/crew_membership.rb +++ b/app/models/crew_membership.rb @@ -6,6 +6,8 @@ class CrewMembership < ApplicationRecord enum :role, { member: 0, vice_captain: 1, captain: 2 } + before_validation :set_joined_at, on: :create + validates :user_id, uniqueness: { scope: :crew_id } validate :one_active_crew_per_user, on: :create validate :captain_limit @@ -14,6 +16,13 @@ class CrewMembership < ApplicationRecord scope :active, -> { where(retired: false) } scope :retired, -> { where(retired: true) } + # Members who were active during a date range (either still active, or retired after the end date) + # Uses joined_at (editable) instead of created_at (system timestamp) for historical accuracy + scope :active_during, ->(start_date, end_date) { + where('retired = false OR retired_at >= ?', start_date) + .where('joined_at <= ?', end_date) + } + def retire! update!(retired: true, retired_at: Time.current, role: :member) end @@ -24,6 +33,10 @@ class CrewMembership < ApplicationRecord private + def set_joined_at + self.joined_at ||= Time.current + end + def one_active_crew_per_user return if retired diff --git a/app/models/phantom_player.rb b/app/models/phantom_player.rb index 93bbcb0..6e681f1 100644 --- a/app/models/phantom_player.rb +++ b/app/models/phantom_player.rb @@ -7,6 +7,8 @@ class PhantomPlayer < ApplicationRecord has_many :gw_individual_scores, dependent: :nullify + before_validation :set_joined_at, on: :create + validates :name, presence: true, length: { maximum: 100 } validates :granblue_id, length: { maximum: 20 }, allow_blank: true validates :granblue_id, uniqueness: { scope: :crew_id }, if: -> { granblue_id.present? } @@ -17,6 +19,15 @@ class PhantomPlayer < ApplicationRecord scope :unclaimed, -> { where(claimed_by_id: nil) } scope :claimed, -> { where.not(claimed_by_id: nil) } scope :pending_confirmation, -> { claimed.where(claim_confirmed: false) } + scope :active, -> { where(retired: false) } + scope :retired, -> { where(retired: true) } + + # Phantoms who were active during a date range (either still active, or retired after the end date) + # Uses joined_at (editable) instead of created_at for historical accuracy + scope :active_during, ->(start_date, end_date) { + where('retired = false OR retired_at >= ?', start_date) + .where('joined_at <= ?', end_date) + } # Assign this phantom to a user (officer action) def assign_to(user) @@ -43,8 +54,17 @@ class PhantomPlayer < ApplicationRecord save! end + # Retire the phantom player (keeps scores) + def retire! + update!(retired: true, retired_at: Time.current) + end + private + def set_joined_at + self.joined_at ||= Time.current + end + def claimed_by_must_be_crew_member return unless claimed_by.present? return if claimed_by.crew == crew diff --git a/db/migrate/20251204102935_add_joined_at_to_crew_memberships_and_phantom_players.rb b/db/migrate/20251204102935_add_joined_at_to_crew_memberships_and_phantom_players.rb new file mode 100644 index 0000000..d9c4d78 --- /dev/null +++ b/db/migrate/20251204102935_add_joined_at_to_crew_memberships_and_phantom_players.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddJoinedAtToCrewMembershipsAndPhantomPlayers < ActiveRecord::Migration[8.0] + def up + add_column :crew_memberships, :joined_at, :datetime + add_column :phantom_players, :joined_at, :datetime + + # Backfill joined_at from created_at for existing records + execute <<-SQL + UPDATE crew_memberships SET joined_at = created_at WHERE joined_at IS NULL + SQL + execute <<-SQL + UPDATE phantom_players SET joined_at = created_at WHERE joined_at IS NULL + SQL + end + + def down + remove_column :crew_memberships, :joined_at + remove_column :phantom_players, :joined_at + end +end