Skip to content

Commit 1fd409f

Browse files
committed
fix(tactical-board): accept frontend field names and fix champion position priority
1 parent 6870e75 commit 1fd409f

1 file changed

Lines changed: 172 additions & 130 deletions

File tree

app/modules/strategy/controllers/tactical_boards_controller.rb

Lines changed: 172 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,194 @@
11
# frozen_string_literal: true
22

3-
module Api
4-
module V1
5-
module Strategy
6-
# Tactical Boards Controller
7-
# Manages tactical board snapshots with player positions and annotations
8-
class TacticalBoardsController < Api::V1::BaseController
9-
before_action :set_tactical_board, only: %i[show update destroy statistics]
10-
11-
# GET /api/v1/strategy/tactical_boards
12-
def index
13-
boards = organization_scoped(TacticalBoard).includes(:created_by, :updated_by, :match, :scrim)
14-
boards = apply_filters(boards)
15-
boards = apply_sorting(boards)
16-
17-
result = paginate(boards)
18-
19-
render_success({
20-
tactical_boards: Strategy::Serializers::TacticalBoardSerializer.render_as_hash(result[:data]),
21-
total: result[:pagination][:total_count],
22-
page: result[:pagination][:current_page],
23-
per_page: result[:pagination][:per_page],
24-
total_pages: result[:pagination][:total_pages]
25-
})
3+
module Strategy
4+
module Controllers
5+
# Tactical Boards Controller
6+
# Manages tactical board snapshots with player positions and annotations
7+
class TacticalBoardsController < Api::V1::BaseController
8+
before_action :set_tactical_board, only: %i[show update destroy statistics]
9+
10+
# GET /api/v1/strategy/tactical_boards
11+
def index
12+
boards = organization_scoped(TacticalBoard).includes(:organization, :created_by, :updated_by, :match, :scrim)
13+
boards = apply_filters(boards)
14+
boards = apply_sorting(boards)
15+
16+
result = paginate(boards)
17+
18+
render_success({
19+
tactical_boards: TacticalBoardSerializer.render_as_hash(result[:data]),
20+
total: result[:pagination][:total_count],
21+
page: result[:pagination][:current_page],
22+
per_page: result[:pagination][:per_page],
23+
total_pages: result[:pagination][:total_pages]
24+
})
25+
end
26+
27+
# GET /api/v1/strategy/tactical_boards/:id
28+
def show
29+
render_success({
30+
tactical_board: TacticalBoardSerializer.render_as_hash(@tactical_board)
31+
})
32+
end
33+
34+
# POST /api/v1/strategy/tactical_boards
35+
def create
36+
board_params = tactical_board_params
37+
board = organization_scoped(TacticalBoard).new
38+
board.assign_attributes(board_params.to_h)
39+
board.organization = current_organization
40+
board.created_by = current_user
41+
board.updated_by = current_user
42+
43+
if board.save
44+
log_user_action(
45+
action: 'create',
46+
entity_type: 'TacticalBoard',
47+
entity_id: board.id,
48+
new_values: board.attributes
49+
)
50+
51+
render_created({
52+
tactical_board: TacticalBoardSerializer.render_as_hash(board)
53+
}, message: 'Tactical board created successfully')
54+
else
55+
render_error(
56+
message: 'Failed to create tactical board',
57+
code: 'VALIDATION_ERROR',
58+
status: :unprocessable_entity,
59+
details: board.errors.as_json
60+
)
2661
end
62+
end
63+
64+
# PATCH /api/v1/strategy/tactical_boards/:id
65+
def update
66+
old_values = @tactical_board.attributes.dup
67+
@tactical_board.updated_by = current_user
68+
69+
if @tactical_board.update(tactical_board_params)
70+
log_user_action(
71+
action: 'update',
72+
entity_type: 'TacticalBoard',
73+
entity_id: @tactical_board.id,
74+
old_values: old_values,
75+
new_values: @tactical_board.attributes
76+
)
2777

28-
# GET /api/v1/strategy/tactical_boards/:id
29-
def show
30-
render_success({
31-
tactical_board: Strategy::Serializers::TacticalBoardSerializer.render_as_hash(@tactical_board)
78+
render_updated({
79+
tactical_board: TacticalBoardSerializer.render_as_hash(@tactical_board)
3280
})
81+
else
82+
render_error(
83+
message: 'Failed to update tactical board',
84+
code: 'VALIDATION_ERROR',
85+
status: :unprocessable_entity,
86+
details: @tactical_board.errors.as_json
87+
)
3388
end
89+
end
3490

35-
# POST /api/v1/strategy/tactical_boards
36-
def create
37-
board = organization_scoped(TacticalBoard).new(tactical_board_params)
38-
board.organization = current_organization
39-
board.created_by = current_user
40-
board.updated_by = current_user
41-
42-
if board.save
43-
log_user_action(
44-
action: 'create',
45-
entity_type: 'TacticalBoard',
46-
entity_id: board.id,
47-
new_values: board.attributes
48-
)
49-
50-
render_created({
51-
tactical_board: Strategy::Serializers::TacticalBoardSerializer.render_as_hash(board)
52-
}, message: 'Tactical board created successfully')
53-
else
54-
render_error(
55-
message: 'Failed to create tactical board',
56-
code: 'VALIDATION_ERROR',
57-
status: :unprocessable_entity,
58-
details: board.errors.as_json
59-
)
60-
end
61-
end
91+
# DELETE /api/v1/strategy/tactical_boards/:id
92+
def destroy
93+
if @tactical_board.destroy
94+
log_user_action(
95+
action: 'delete',
96+
entity_type: 'TacticalBoard',
97+
entity_id: @tactical_board.id,
98+
old_values: @tactical_board.attributes
99+
)
62100

63-
# PATCH /api/v1/strategy/tactical_boards/:id
64-
def update
65-
old_values = @tactical_board.attributes.dup
66-
@tactical_board.updated_by = current_user
67-
68-
if @tactical_board.update(tactical_board_params)
69-
log_user_action(
70-
action: 'update',
71-
entity_type: 'TacticalBoard',
72-
entity_id: @tactical_board.id,
73-
old_values: old_values,
74-
new_values: @tactical_board.attributes
75-
)
76-
77-
render_updated({
78-
tactical_board: Strategy::Serializers::TacticalBoardSerializer.render_as_hash(@tactical_board)
79-
})
80-
else
81-
render_error(
82-
message: 'Failed to update tactical board',
83-
code: 'VALIDATION_ERROR',
84-
status: :unprocessable_entity,
85-
details: @tactical_board.errors.as_json
86-
)
87-
end
101+
render_deleted(message: 'Tactical board deleted successfully')
102+
else
103+
render_error(
104+
message: 'Failed to delete tactical board',
105+
code: 'DELETE_ERROR',
106+
status: :unprocessable_entity
107+
)
88108
end
109+
end
89110

90-
# DELETE /api/v1/strategy/tactical_boards/:id
91-
def destroy
92-
if @tactical_board.destroy
93-
log_user_action(
94-
action: 'delete',
95-
entity_type: 'TacticalBoard',
96-
entity_id: @tactical_board.id,
97-
old_values: @tactical_board.attributes
98-
)
99-
100-
render_deleted(message: 'Tactical board deleted successfully')
101-
else
102-
render_error(
103-
message: 'Failed to delete tactical board',
104-
code: 'DELETE_ERROR',
105-
status: :unprocessable_entity
106-
)
107-
end
108-
end
111+
# GET /api/v1/strategy/tactical_boards/:id/statistics
112+
def statistics
113+
stats = @tactical_board.statistics
109114

110-
# GET /api/v1/strategy/tactical_boards/:id/statistics
111-
def statistics
112-
stats = @tactical_board.statistics
115+
render_success({
116+
tactical_board_id: @tactical_board.id,
117+
statistics: stats
118+
})
119+
end
113120

114-
render_success({
115-
tactical_board_id: @tactical_board.id,
116-
statistics: stats
117-
})
118-
end
121+
private
119122

120-
private
123+
def set_tactical_board
124+
@tactical_board = organization_scoped(TacticalBoard).find(params[:id])
125+
end
121126

122-
def set_tactical_board
123-
@tactical_board = organization_scoped(TacticalBoard).find(params[:id])
124-
end
127+
def apply_filters(boards)
128+
boards = boards.for_match(params[:match_id]) if params[:match_id].present?
129+
boards = boards.for_scrim(params[:scrim_id]) if params[:scrim_id].present?
130+
boards = boards.by_time(params[:game_time]) if params[:game_time].present?
131+
boards
132+
end
125133

126-
def apply_filters(boards)
127-
boards = boards.for_match(params[:match_id]) if params[:match_id].present?
128-
boards = boards.for_scrim(params[:scrim_id]) if params[:scrim_id].present?
129-
boards = boards.by_time(params[:game_time]) if params[:game_time].present?
130-
boards
131-
end
134+
def apply_sorting(boards)
135+
sort_by = params[:sort_by] || 'created_at'
136+
sort_order = params[:sort_order]&.downcase == 'asc' ? :asc : :desc
132137

133-
def apply_sorting(boards)
134-
sort_by = params[:sort_by] || 'created_at'
135-
sort_order = params[:sort_order]&.downcase == 'asc' ? :asc : :desc
138+
boards.order(sort_by => sort_order)
139+
end
136140

137-
boards.order(sort_by => sort_order)
141+
def tactical_board_params
142+
# Support both nested format (tactical_board: {title:...}) and flat format (name:..., board_state:...)
143+
# The frontend may send data at the top level with different field names.
144+
tb = params[:tactical_board]
145+
source = tb.present? && (tb[:title].present? || tb[:name].present?) ? tb : params
146+
147+
permitted = {
148+
title: source[:title] || source[:name],
149+
match_id: source[:match_id],
150+
scrim_id: source[:scrim_id],
151+
game_time: source[:game_time]
152+
}.compact
153+
154+
# Accept map_state or board_state
155+
map = source[:map_state] || source[:board_state]
156+
permitted[:map_state] = map.as_json if map.present?
157+
158+
# Accept annotations
159+
permitted[:annotations] = source[:annotations].as_json if source[:annotations].present?
160+
161+
# Merge champion_selections into map_state.players.
162+
# board_state (already in permitted[:map_state]) carries the authoritative positions
163+
# from the rendered canvas (drag results). champion_selections carries identity
164+
# (champion name, role). For each slot, use:
165+
# 1. x/y from champion_selection if explicitly provided
166+
# 2. x/y from board_state.players[i] as fallback (preserves drag position)
167+
# 3. 50 as last resort default
168+
selections = source[:champion_selections]
169+
if selections.present? && selections.is_a?(Array)
170+
existing_players = permitted.dig(:map_state, 'players') || []
171+
172+
permitted[:map_state] ||= { 'players' => [] }
173+
permitted[:map_state]['players'] = selections.map.with_index do |cs, idx|
174+
existing = existing_players[idx] || {}
175+
176+
# board_state (existing[]) represents the live canvas after a drag — it
177+
# always wins for position. champion_selections x/y is only a fallback
178+
# for the initial placement when board_state has no entry yet.
179+
cs_x = cs[:x].nil? ? cs['x'] : cs[:x]
180+
cs_y = cs[:y].nil? ? cs['y'] : cs[:y]
181+
182+
{
183+
'champion' => cs[:champion] || cs['champion'] || existing['champion'],
184+
'role' => cs[:role] || cs['role'] || existing['role'],
185+
'x' => (existing['x'] || cs_x || 50).to_f,
186+
'y' => (existing['y'] || cs_y || 50).to_f
187+
}
188+
end
138189
end
139190

140-
def tactical_board_params
141-
params.require(:tactical_board).permit(
142-
:title,
143-
:match_id,
144-
:scrim_id,
145-
:game_time,
146-
map_state: {},
147-
annotations: []
148-
)
149-
end
191+
permitted
150192
end
151193
end
152194
end

0 commit comments

Comments
 (0)