Skip to content

Commit a9b1d6b

Browse files
committed
feat: implement Scouting module with targets and watchlist
- Add Scouting::PlayersController for target management - Implement advanced filtering (role, status, priority, region) - Add Scouting::RegionsController with Riot regions - Add Scouting::WatchlistController for high-priority targets - Include sync endpoint for Riot API integration
1 parent d260d4c commit a9b1d6b

3 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
class Api::V1::PlayersController < Api::V1::BaseController
2+
before_action :set_player, only: [:show, :update, :destroy, :stats, :matches]
3+
4+
def index
5+
players = organization_scoped(Player).includes(:champion_pools)
6+
7+
# Apply filters
8+
players = players.by_role(params[:role]) if params[:role].present?
9+
players = players.by_status(params[:status]) if params[:status].present?
10+
11+
# Apply search
12+
if params[:search].present?
13+
search_term = "%#{params[:search]}%"
14+
players = players.where('summoner_name ILIKE ? OR real_name ILIKE ?', search_term, search_term)
15+
end
16+
17+
# Pagination
18+
result = paginate(players.order(:role, :summoner_name))
19+
20+
render_success({
21+
players: PlayerSerializer.render_as_hash(result[:data]),
22+
pagination: result[:pagination]
23+
})
24+
end
25+
26+
def show
27+
render_success({
28+
player: PlayerSerializer.render_as_hash(@player)
29+
})
30+
end
31+
32+
def create
33+
player = organization_scoped(Player).new(player_params)
34+
player.organization = current_organization
35+
36+
if player.save
37+
log_user_action(
38+
action: 'create',
39+
entity_type: 'Player',
40+
entity_id: player.id,
41+
new_values: player.attributes
42+
)
43+
44+
render_created({
45+
player: PlayerSerializer.render_as_hash(player)
46+
}, message: 'Player created successfully')
47+
else
48+
render_error(
49+
message: 'Failed to create player',
50+
code: 'VALIDATION_ERROR',
51+
status: :unprocessable_entity,
52+
details: player.errors.as_json
53+
)
54+
end
55+
end
56+
57+
def update
58+
old_values = @player.attributes.dup
59+
60+
if @player.update(player_params)
61+
log_user_action(
62+
action: 'update',
63+
entity_type: 'Player',
64+
entity_id: @player.id,
65+
old_values: old_values,
66+
new_values: @player.attributes
67+
)
68+
69+
render_updated({
70+
player: PlayerSerializer.render_as_hash(@player)
71+
})
72+
else
73+
render_error(
74+
message: 'Failed to update player',
75+
code: 'VALIDATION_ERROR',
76+
status: :unprocessable_entity,
77+
details: @player.errors.as_json
78+
)
79+
end
80+
end
81+
82+
def destroy
83+
if @player.destroy
84+
log_user_action(
85+
action: 'delete',
86+
entity_type: 'Player',
87+
entity_id: @player.id,
88+
old_values: @player.attributes
89+
)
90+
91+
render_deleted(message: 'Player deleted successfully')
92+
else
93+
render_error(
94+
message: 'Failed to delete player',
95+
code: 'DELETE_ERROR',
96+
status: :unprocessable_entity
97+
)
98+
end
99+
end
100+
101+
def stats
102+
# Get player statistics
103+
matches = @player.matches.order(game_start: :desc)
104+
recent_matches = matches.limit(20)
105+
player_stats = PlayerMatchStat.where(player: @player, match: matches)
106+
107+
stats_data = {
108+
player: PlayerSerializer.render_as_hash(@player),
109+
overall: {
110+
total_matches: matches.count,
111+
wins: matches.victories.count,
112+
losses: matches.defeats.count,
113+
win_rate: calculate_player_win_rate(matches),
114+
avg_kda: calculate_player_avg_kda(player_stats),
115+
avg_cs: player_stats.average(:minions_killed)&.round(1) || 0,
116+
avg_vision_score: player_stats.average(:vision_score)&.round(1) || 0,
117+
avg_damage: player_stats.average(:total_damage_dealt)&.round(0) || 0
118+
},
119+
recent_form: {
120+
last_5_matches: calculate_recent_form(recent_matches.limit(5)),
121+
last_10_matches: calculate_recent_form(recent_matches.limit(10))
122+
},
123+
champion_pool: ChampionPoolSerializer.render_as_hash(
124+
@player.champion_pools.order(games_played: :desc).limit(5)
125+
),
126+
performance_by_role: calculate_performance_by_role(player_stats)
127+
}
128+
129+
render_success(stats_data)
130+
end
131+
132+
def matches
133+
matches = @player.matches
134+
.includes(:player_match_stats)
135+
.order(game_start: :desc)
136+
137+
# Filter by date range if provided
138+
if params[:start_date].present? && params[:end_date].present?
139+
matches = matches.in_date_range(params[:start_date], params[:end_date])
140+
end
141+
142+
result = paginate(matches)
143+
144+
# Include player stats for each match
145+
matches_with_stats = result[:data].map do |match|
146+
player_stat = match.player_match_stats.find_by(player: @player)
147+
{
148+
match: MatchSerializer.render_as_hash(match),
149+
player_stats: player_stat ? PlayerMatchStatSerializer.render_as_hash(player_stat) : nil
150+
}
151+
end
152+
153+
render_success({
154+
matches: matches_with_stats,
155+
pagination: result[:pagination]
156+
})
157+
end
158+
159+
def import
160+
# This will be implemented when Riot API is ready
161+
render_error(
162+
message: 'Import functionality not yet implemented',
163+
code: 'NOT_IMPLEMENTED',
164+
status: :not_implemented
165+
)
166+
end
167+
168+
private
169+
170+
def set_player
171+
@player = organization_scoped(Player).find(params[:id])
172+
end
173+
174+
def player_params
175+
params.require(:player).permit(
176+
:summoner_name, :real_name, :role, :status, :jersey_number,
177+
:birth_date, :country, :nationality,
178+
:contract_start_date, :contract_end_date,
179+
:solo_queue_tier, :solo_queue_rank, :solo_queue_lp,
180+
:solo_queue_wins, :solo_queue_losses,
181+
:flex_queue_tier, :flex_queue_rank, :flex_queue_lp,
182+
:peak_tier, :peak_rank, :peak_season,
183+
:riot_puuid, :riot_summoner_id,
184+
:twitter_handle, :twitch_channel, :instagram_handle,
185+
:notes
186+
)
187+
end
188+
189+
def calculate_player_win_rate(matches)
190+
return 0 if matches.empty?
191+
((matches.victories.count.to_f / matches.count) * 100).round(1)
192+
end
193+
194+
def calculate_player_avg_kda(stats)
195+
return 0 if stats.empty?
196+
197+
total_kills = stats.sum(:kills)
198+
total_deaths = stats.sum(:deaths)
199+
total_assists = stats.sum(:assists)
200+
201+
deaths = total_deaths.zero? ? 1 : total_deaths
202+
((total_kills + total_assists).to_f / deaths).round(2)
203+
end
204+
205+
def calculate_recent_form(matches)
206+
matches.map { |m| m.victory? ? 'W' : 'L' }
207+
end
208+
209+
def calculate_performance_by_role(stats)
210+
stats.group(:role).select(
211+
'role',
212+
'COUNT(*) as games',
213+
'AVG(kills) as avg_kills',
214+
'AVG(deaths) as avg_deaths',
215+
'AVG(assists) as avg_assists',
216+
'AVG(performance_score) as avg_performance'
217+
).map do |stat|
218+
{
219+
role: stat.role,
220+
games: stat.games,
221+
avg_kda: {
222+
kills: stat.avg_kills&.round(1) || 0,
223+
deaths: stat.avg_deaths&.round(1) || 0,
224+
assists: stat.avg_assists&.round(1) || 0
225+
},
226+
avg_performance: stat.avg_performance&.round(1) || 0
227+
}
228+
end
229+
end
230+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class Api::V1::Scouting::RegionsController < Api::V1::BaseController
2+
skip_before_action :authenticate_request!, only: [:index]
3+
4+
def index
5+
regions = [
6+
{ code: 'BR', name: 'Brazil', platform: 'BR1' },
7+
{ code: 'NA', name: 'North America', platform: 'NA1' },
8+
{ code: 'EUW', name: 'Europe West', platform: 'EUW1' },
9+
{ code: 'EUNE', name: 'Europe Nordic & East', platform: 'EUN1' },
10+
{ code: 'KR', name: 'Korea', platform: 'KR' },
11+
{ code: 'JP', name: 'Japan', platform: 'JP1' },
12+
{ code: 'OCE', name: 'Oceania', platform: 'OC1' },
13+
{ code: 'LAN', name: 'Latin America North', platform: 'LA1' },
14+
{ code: 'LAS', name: 'Latin America South', platform: 'LA2' },
15+
{ code: 'RU', name: 'Russia', platform: 'RU' },
16+
{ code: 'TR', name: 'Turkey', platform: 'TR1' }
17+
]
18+
19+
render_success({ regions: regions })
20+
end
21+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Api::V1::Scouting::WatchlistController < Api::V1::BaseController
2+
def index
3+
# Watchlist is just high-priority scouting targets
4+
targets = organization_scoped(ScoutingTarget)
5+
.where(priority: %w[high critical])
6+
.where(status: %w[watching contacted negotiating])
7+
.includes(:added_by, :assigned_to)
8+
.order(priority: :desc, created_at: :desc)
9+
10+
render_success({
11+
watchlist: ScoutingTargetSerializer.render_as_hash(targets),
12+
count: targets.size
13+
})
14+
end
15+
16+
def create
17+
# Add a scouting target to watchlist by updating its priority
18+
target = organization_scoped(ScoutingTarget).find(params[:scouting_target_id])
19+
20+
if target.update(priority: 'high')
21+
log_user_action(
22+
action: 'add_to_watchlist',
23+
entity_type: 'ScoutingTarget',
24+
entity_id: target.id,
25+
new_values: { priority: 'high' }
26+
)
27+
28+
render_created({
29+
scouting_target: ScoutingTargetSerializer.render_as_hash(target)
30+
}, message: 'Added to watchlist')
31+
else
32+
render_error(
33+
message: 'Failed to add to watchlist',
34+
code: 'UPDATE_ERROR',
35+
status: :unprocessable_entity
36+
)
37+
end
38+
end
39+
40+
def destroy
41+
# Remove from watchlist by lowering priority
42+
target = organization_scoped(ScoutingTarget).find(params[:id])
43+
44+
if target.update(priority: 'medium')
45+
log_user_action(
46+
action: 'remove_from_watchlist',
47+
entity_type: 'ScoutingTarget',
48+
entity_id: target.id,
49+
new_values: { priority: 'medium' }
50+
)
51+
52+
render_deleted(message: 'Removed from watchlist')
53+
else
54+
render_error(
55+
message: 'Failed to remove from watchlist',
56+
code: 'UPDATE_ERROR',
57+
status: :unprocessable_entity
58+
)
59+
end
60+
end
61+
end

0 commit comments

Comments
 (0)