Skip to content

Commit

Permalink
Add files todo and update
Browse files Browse the repository at this point in the history
**Done:** Models.py, list.html (?), add.html, field-2025.png

**In Progress**: Edit.html

**Not started**: Leaderboard.html, matches.html, team.html
cherriae committed Jan 7, 2025
1 parent c13ca9c commit 48e60ec
Showing 10 changed files with 1,009 additions and 387 deletions.
54 changes: 41 additions & 13 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -71,19 +71,39 @@ def __init__(self, data):
self.team_number = data.get('team_number')
self.match_number = data.get('match_number')
self.event_code = data.get('event_code')
self.auto_points = data.get('auto_points', 0)
self.teleop_points = data.get('teleop_points', 0)
self.endgame_points = data.get('endgame_points', 0)
self.total_points = data.get('total_points', 0)
self.notes = data.get('notes', '')
self.alliance = data.get('alliance', '')
self.match_result = data.get('match_result', '')

# Coral scoring
self.coral_level1 = data.get('coral_level1', 0)
self.coral_level2 = data.get('coral_level2', 0)
self.coral_level3 = data.get('coral_level3', 0)
self.coral_level4 = data.get('coral_level4', 0)

# Algae scoring
self.algae_net = data.get('algae_net', 0)
self.algae_processor = data.get('algae_processor', 0)
self.human_player = data.get('human_player', 0) # Number of successful shots

# Climb
self.climb_type = data.get('climb_type', '') # 'shallow', 'deep', 'park', or ''
self.climb_success = data.get('climb_success', False)

# Defense
self.defense_rating = data.get('defense_rating', 1) # 1-5 scale
self.defense_notes = data.get('defense_notes', '')

# Auto
self.auto_path = data.get('auto_path', '') # Store canvas data as base64
self.auto_notes = data.get('auto_notes', '')

# Notes
self.notes = data.get('notes', '')

# Scouter information
self.scouter_id = data.get('scouter_id')
self.scouter_name = data.get('scouter_name')
self.scouter_team = data.get('scouter_team')
self.is_owner = data.get('is_owner', True) # Default to False if not set
self.is_owner = data.get('is_owner', True)

@classmethod
def create_from_db(cls, data):
@@ -95,13 +115,21 @@ def to_dict(self):
'team_number': self.team_number,
'match_number': self.match_number,
'event_code': self.event_code,
'auto_points': self.auto_points,
'teleop_points': self.teleop_points,
'endgame_points': self.endgame_points,
'total_points': self.total_points,
'notes': self.notes,
'alliance': self.alliance,
'match_result': self.match_result,
'coral_level1': self.coral_level1,
'coral_level2': self.coral_level2,
'coral_level3': self.coral_level3,
'coral_level4': self.coral_level4,
'algae_net': self.algae_net,
'algae_processor': self.algae_processor,
'human_player': self.human_player,
'climb_type': self.climb_type,
'climb_success': self.climb_success,
'defense_rating': self.defense_rating,
'defense_notes': self.defense_notes,
'auto_path': self.auto_path,
'auto_notes': self.auto_notes,
'notes': self.notes,
'scouter_id': self.scouter_id,
'scouter_name': self.scouter_name,
'scouter_team': self.scouter_team,
223 changes: 151 additions & 72 deletions app/scout/routes.py
Original file line number Diff line number Diff line change
@@ -31,7 +31,8 @@ def wrapper(*args, **kwargs):
@login_required
def add_scouting_data():
if request.method == "POST":
data = request.get_json() if request.is_json else request.form
data = request.get_json() if request.is_json else request.form.to_dict()

success, message = scouting_manager.add_scouting_data(
data, current_user.get_id()
)
@@ -175,9 +176,21 @@ async def compare_teams():
{
"event_code": entry["event_code"],
"match_number": entry["match_number"],
"auto_points": entry["auto_points"],
"teleop_points": entry["teleop_points"],
"endgame_points": entry["endgame_points"],
"coral_levels": [
entry["coral_level1"],
entry["coral_level2"],
entry["coral_level3"],
entry["coral_level4"]
],
"algae": {
"net": entry["algae_net"],
"processor": entry["algae_processor"],
"human_player": entry["human_player"]
},
"climb": {
"type": entry["climb_type"],
"success": entry["climb_success"]
},
"total_points": entry["total_points"],
"notes": entry["notes"],
"scouter": entry["scouter"]["username"],
@@ -192,7 +205,14 @@ async def compare_teams():
"city": team.get("city"),
"state_prov": team.get("state_prov"),
"country": team.get("country"),
"stats": stats,
"stats": {
"matches_played": stats["matches_played"],
"avg_coral": stats["avg_coral"],
"avg_algae": stats["avg_algae"],
"climb_success_rate": stats["climb_success_rate"],
"defense_rating": stats["avg_defense"],
"total_points": stats["total_points"]
},
"scouting_data": scouting_entries,
}

@@ -236,13 +256,94 @@ async def search_teams():
"from": "users",
"localField": "scouter_id",
"foreignField": "_id",
"as": "scouter",
"as": "scouter"
}
},
{"$unwind": "$scouter"},
{
"$project": {
"_id": 1,
"team_number": 1,
"match_number": 1,
"event_code": 1,
"coral_level1": 1,
"coral_level2": 1,
"coral_level3": 1,
"coral_level4": 1,
"algae_net": 1,
"algae_processor": 1,
"human_player": 1,
"climb_type": 1,
"climb_success": 1,
"defense_rating": 1,
"defense_notes": 1,
"auto_path": 1,
"auto_notes": 1,
"total_points": 1,
"notes": 1,
"alliance": 1,
"scouter_id": 1,
"scouter_name": "$scouter.username",
"scouter_team": "$scouter.teamNumber"
}
}
]

team_scouting_data = list(scouting_manager.db.team_data.aggregate(pipeline))

# Calculate statistics
matches_played = len(team_scouting_data)
if matches_played > 0:
coral_totals = [sum([
entry["coral_level1"],
entry["coral_level2"],
entry["coral_level3"],
entry["coral_level4"]
]) for entry in team_scouting_data]

algae_totals = [
entry["algae_net"] + entry["algae_processor"]
for entry in team_scouting_data
]

successful_climbs = sum(bool(entry["climb_success"])
for entry in team_scouting_data)

stats = {
"matches_played": matches_played,
"coral_stats": {
"level1": sum(entry["coral_level1"] for entry in team_scouting_data) / matches_played,
"level2": sum(entry["coral_level2"] for entry in team_scouting_data) / matches_played,
"level3": sum(entry["coral_level3"] for entry in team_scouting_data) / matches_played,
"level4": sum(entry["coral_level4"] for entry in team_scouting_data) / matches_played,
},
"algae_stats": {
"net": sum(entry["algae_net"] for entry in team_scouting_data) / matches_played,
"processor": sum(entry["algae_processor"] for entry in team_scouting_data) / matches_played,
"human_player_rate": sum(bool(entry["human_player"])
for entry in team_scouting_data) / matches_played
},
"climb_success_rate": sum(bool(entry["climb_success"])
for entry in team_scouting_data) / matches_played,
"avg_defense": sum(entry["defense_rating"] for entry in team_scouting_data) / matches_played
}
else:
stats = {
"matches_played": 0,
"coral_stats": {
"level1": 0,
"level2": 0,
"level3": 0,
"level4": 0
},
"algae_stats": {
"net": 0,
"processor": 0,
"human_player_rate": 0
},
"climb_success_rate": 0,
"avg_defense": 0
}

scouting_entries = [
{
@@ -391,9 +492,19 @@ def matches():
"number": "$team_number",
"total_points": "$total_points",
"alliance": "$alliance",
"auto_points": "$auto_points",
"teleop_points": "$teleop_points",
"endgame_points": "$endgame_points"
"coral_total": {
"$sum": [
"$coral_level1",
"$coral_level2",
"$coral_level3",
"$coral_level4"
]
},
"algae_total": {
"$sum": ["$algae_net", "$algae_processor"]
},
"climb_type": "$climb_type",
"climb_success": "$climb_success"
}
}
}
@@ -414,83 +525,51 @@ def matches():
"red_teams": red_teams,
"blue_teams": blue_teams,
"red_score": sum(t["total_points"] for t in red_teams),
"blue_score": sum(t["total_points"] for t in blue_teams)
"blue_score": sum(t["total_points"] for t in blue_teams),
"red_coral_total": sum(t["coral_total"] for t in red_teams),
"red_algae_total": sum(t["algae_total"] for t in red_teams),
"blue_coral_total": sum(t["coral_total"] for t in blue_teams),
"blue_algae_total": sum(t["algae_total"] for t in blue_teams)
})

return render_template("scouting/matches.html", matches=matches)
except Exception as e:
flash(f"Error fetching matches: {str(e)}", "error")
return render_template("scouting/matches.html", matches=[])

@scouting_bp.route("/scouting/team/<int:team_number>")
@scouting_bp.route("/team/<int:team_number>")
@login_required
def team_view(team_number):
def view_team(team_number):
try:
# Get team stats from MongoDB
pipeline = [
{"$match": {"team_number": team_number}},
{
"$group": {
"_id": "$team_number",
"matches_played": {"$sum": 1},
"total_points": {"$sum": "$total_points"},
"auto_points": {"$avg": "$auto_points"},
"teleop_points": {"$avg": "$teleop_points"},
"endgame_points": {"$avg": "$endgame_points"},
"matches": {
"$push": {
"match_number": "$match_number",
"event_code": "$event_code",
"auto_points": "$auto_points",
"teleop_points": "$teleop_points",
"endgame_points": "$endgame_points",
"total_points": "$total_points",
"notes": {"$ifNull": ["$notes", ""]}
}
}
}
}
]

team_data = list(scouting_manager.db.team_data.aggregate(pipeline))

if not team_data:
flash("No scouting data found for this team", "error")
return redirect(url_for("scouting.list_scouting_data"))

team_stats = team_data[0]

# Convert Decimal128 to float for JSON serialization
stats = {
"matches_played": team_stats["matches_played"],
"total_points": float(team_stats["total_points"]),
"auto_points": float(team_stats["auto_points"]),
"teleop_points": float(team_stats["teleop_points"]),
"endgame_points": float(team_stats["endgame_points"])
}

matches = [
{
"event_code": str(match["event_code"]),
"match_number": int(match["match_number"]),
"auto_points": float(match["auto_points"]),
"teleop_points": float(match["teleop_points"]),
"endgame_points": float(match["endgame_points"]),
"total_points": float(match["total_points"]),
"notes": str(match["notes"]),
}
for match in team_stats["matches"]
]
matches = scouting_manager.get_team_matches(team_number)
stats = scouting_manager.get_team_stats(team_number)

# Calculate averages and success rates
if stats["matches_played"] > 0:
stats["avg_coral"] = (
stats["total_coral"] / stats["matches_played"]
)
stats["avg_algae"] = (
stats["total_algae"] / stats["matches_played"]
)
stats["climb_success_rate"] = (
stats["successful_climbs"] / stats["matches_played"]
)
else:
stats.update({
"avg_coral": 0,
"avg_algae": 0,
"climb_success_rate": 0
})

return render_template(
"scouting/team.html",
team_number=team_number,
stats=stats,
matches=matches
matches=matches,
stats=stats
)

except Exception as e:
print(f"Error loading team data: {str(e)}")
flash(f"Error loading team data: {str(e)}", "error")
flash(f"Error fetching team data: {str(e)}", "error")
return redirect(url_for("scouting.list_scouting_data"))

@scouting_bp.route("/scouting/check_team")
207 changes: 150 additions & 57 deletions app/scout/scouting_utils.py
Original file line number Diff line number Diff line change
@@ -105,55 +105,49 @@ def add_scouting_data(self, data, scouter_id):
if (alliance == "red" and len(red_teams) >= 3) or (alliance == "blue" and len(blue_teams) >= 3):
return False, f"Cannot add more teams to {alliance} alliance (maximum 3)"

# Calculate alliance scores
red_score = sum(t["total_points"] for t in red_teams)
blue_score = sum(t["total_points"] for t in blue_teams)

# Add current team's points to their alliance
current_points = (
int(data["auto_points"])
+ int(data["teleop_points"])
+ int(data["endgame_points"])
)
if alliance == "red":
red_score += current_points
alliance_score = red_score
opponent_score = blue_score
else:
blue_score += current_points
alliance_score = blue_score
opponent_score = red_score

# Determine match result
if alliance_score > opponent_score:
match_result = "won"
elif alliance_score < opponent_score:
match_result = "lost"
else:
match_result = "tie"

# Process form data
team_data = {
"team_number": team_number,
"event_code": data["event_code"],
"match_number": int(data["match_number"]),
"auto_points": int(data["auto_points"]),
"teleop_points": int(data["teleop_points"]),
"endgame_points": int(data["endgame_points"]),
"total_points": current_points,
"notes": data["notes"],
"scouter_id": ObjectId(scouter_id),
"alliance": alliance,
"alliance_score": alliance_score,
"opponent_score": opponent_score,
"match_result": match_result,

# Coral scoring
"coral_level1": int(data.get("coral_level1", 0)),
"coral_level2": int(data.get("coral_level2", 0)),
"coral_level3": int(data.get("coral_level3", 0)),
"coral_level4": int(data.get("coral_level4", 0)),

# Algae scoring
"algae_net": int(data.get("algae_net", 0)),
"algae_processor": int(data.get("algae_processor", 0)),
"human_player": int(data.get("human_player", 0)),

# Climb
"climb_type": data.get("climb_type", ""),
"climb_success": bool(data.get("climb_success", False)),

# Defense
"defense_rating": int(data.get("defense_rating", 1)),
"defense_notes": data.get("defense_notes", ""),

# Auto
"auto_path": data.get("auto_path", ""),
"auto_notes": data.get("auto_notes", ""),

# Notes
"notes": data.get("notes", ""),

# Metadata
"scouter_id": ObjectId(scouter_id),
"created_at": datetime.now(timezone.utc),
}

self.db.team_data.insert_one(team_data)
logger.info(f"Added new scouting data for team {data['team_number']}")
return True, "Data added successfully"
result = self.db.team_data.insert_one(team_data)
return True, str(result.inserted_id)

except Exception as e:
logger.error(f"Error adding scouting data: {str(e)}")
logger.error(f"Error adding team data: {str(e)}")
return False, str(e)

@with_mongodb_retry(retries=3, delay=2)
@@ -176,13 +170,21 @@ def get_all_scouting_data(self):
"team_number": 1,
"match_number": 1,
"event_code": 1,
"auto_points": 1,
"teleop_points": 1,
"endgame_points": 1,
"total_points": 1,
"coral_level1": 1,
"coral_level2": 1,
"coral_level3": 1,
"coral_level4": 1,
"algae_net": 1,
"algae_processor": 1,
"human_player": 1,
"climb_type": 1,
"climb_success": 1,
"defense_rating": 1,
"defense_notes": 1,
"auto_path": 1,
"auto_notes": 1,
"notes": 1,
"alliance": 1,
"match_result": 1,
"scouter_id": 1,
"scouter_name": "$scouter.username",
"scouter_team": "$scouter.teamNumber"
@@ -191,9 +193,9 @@ def get_all_scouting_data(self):
]

team_data = list(self.db.team_data.aggregate(pipeline))
return [TeamData.create_from_db(data) for data in team_data]
return team_data
except Exception as e:
print(f"Error fetching team data: {e}")
logger.error(f"Error fetching team data: {str(e)}")
return []

@with_mongodb_retry(retries=3, delay=2)
@@ -237,17 +239,33 @@ def update_team_data(self, team_id, data, scouter_id):
"team_number": int(data["team_number"]),
"event_code": data["event_code"],
"match_number": int(data["match_number"]),
"auto_points": int(data["auto_points"]),
"teleop_points": int(data["teleop_points"]),
"endgame_points": int(data["endgame_points"]),
"total_points": (
int(data["auto_points"])
+ int(data["teleop_points"])
+ int(data["endgame_points"])
),
"notes": data["notes"],
"alliance": data.get("alliance", "red"),
"match_result": data.get("match_result", ""),

# Coral scoring
"coral_level1": int(data.get("coral_level1", 0)),
"coral_level2": int(data.get("coral_level2", 0)),
"coral_level3": int(data.get("coral_level3", 0)),
"coral_level4": int(data.get("coral_level4", 0)),

# Algae scoring
"algae_net": int(data.get("algae_net", 0)),
"algae_processor": int(data.get("algae_processor", 0)),
"human_player": int(data.get("human_player", 0)),

# Climb
"climb_type": data.get("climb_type", ""),
"climb_success": bool(data.get("climb_success", False)),

# Defense
"defense_rating": int(data.get("defense_rating", 1)),
"defense_notes": data.get("defense_notes", ""),

# Auto
"auto_path": data.get("auto_path", ""),
"auto_notes": data.get("auto_notes", ""),

# Notes
"notes": data.get("notes", ""),
}

result = self.db.team_data.update_one(
@@ -283,6 +301,81 @@ def has_team_data(self, team_number):
logger.error(f"Error checking team data: {str(e)}")
return False

@with_mongodb_retry(retries=3, delay=2)
def get_team_stats(self, team_number):
"""Get comprehensive stats for a team"""
self.ensure_connected()
try:
pipeline = [
{"$match": {"team_number": int(team_number)}},
{
"$group": {
"_id": "$team_number",
"matches_played": {"$sum": 1},
"total_coral": {
"$sum": {
"$add": [
"$coral_level1",
"$coral_level2",
"$coral_level3",
"$coral_level4"
]
}
},
"total_algae": {
"$sum": {"$add": ["$algae_net", "$algae_processor"]}
},
"successful_climbs": {
"$sum": {"$cond": ["$climb_success", 1, 0]}
},
"total_defense": {"$sum": "$defense_rating"},
"total_points": {"$sum": "$total_points"}
}
}
]

result = list(self.db.team_data.aggregate(pipeline))
if not result:
return {
"matches_played": 0,
"total_coral": 0,
"total_algae": 0,
"successful_climbs": 0,
"total_defense": 0,
"total_points": 0
}

stats = result[0]
stats.pop("_id") # Remove MongoDB ID
return stats
except Exception as e:
logger.error(f"Error getting team stats: {str(e)}")
return None

@with_mongodb_retry(retries=3, delay=2)
def get_team_matches(self, team_number):
"""Get all match data for a specific team"""
self.ensure_connected()
try:
pipeline = [
{"$match": {"team_number": int(team_number)}},
{"$sort": {"event_code": 1, "match_number": 1}},
{
"$lookup": {
"from": "users",
"localField": "scouter_id",
"foreignField": "_id",
"as": "scouter"
}
},
{"$unwind": "$scouter"}
]

return list(self.db.team_data.aggregate(pipeline))
except Exception as e:
logger.error(f"Error getting team matches: {str(e)}")
return []

def __del__(self):
"""Cleanup MongoDB connection"""
if self.client:
Binary file added app/static/images/field-2025.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
340 changes: 281 additions & 59 deletions app/templates/scouting/add.html

Large diffs are not rendered by default.

303 changes: 185 additions & 118 deletions app/templates/scouting/edit.html

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions app/templates/scouting/leaderboard.html
Original file line number Diff line number Diff line change
@@ -14,10 +14,11 @@ <h1 class="text-2xl font-bold mb-2">Team Leaderboard</h1>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rank</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Team</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Record (W-L-T)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Win Rate</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Matches</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Points</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Coral</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Algae</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Climb %</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Defense</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Points</th>
</tr>
</thead>
@@ -31,20 +32,42 @@ <h1 class="text-2xl font-bold mb-2">Team Leaderboard</h1>
Team {{ team.team_number }}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ team.wins }}-{{ team.losses }}-{{ team.ties }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ team.matches_played }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm">
<div class="font-medium">{{ "%.1f"|format(team.avg_coral) }}</div>
<div class="text-gray-500 text-xs">per match</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm">
<div class="font-medium">{{ "%.1f"|format(team.avg_algae) }}</div>
<div class="text-gray-500 text-xs">per match</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ team.win_rate }}%"></div>
<div class="bg-green-600 h-2 rounded-full"
style="width: {{ team.climb_success_rate }}%">
</div>
</div>
<span class="text-sm text-gray-500">{{ "%.1f%%"|format(team.win_rate) }}</span>
<span class="text-sm text-gray-500">{{ "%.1f%%"|format(team.climb_success_rate) }}</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ team.matches_played }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ "%.1f"|format(team.avg_points) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ team.total_points }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div class="bg-blue-600 h-2 rounded-full"
style="width: {{ (team.avg_defense / 5) * 100 }}%">
</div>
</div>
<span class="text-sm text-gray-500">{{ "%.1f"|format(team.avg_defense) }}/5</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ team.total_points }}
</td>
</tr>
{% endfor %}
</tbody>
140 changes: 110 additions & 30 deletions app/templates/scouting/list.html
Original file line number Diff line number Diff line change
@@ -29,6 +29,31 @@ <h2 class="text-lg font-semibold text-gray-700 mb-2">Pending Entries</h2>
</div>
</div>

<!-- Auto Path Modal -->
<div id="autoPathModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative top-4 mx-auto p-3 border w-[95%] sm:w-[500px] shadow-lg rounded-md bg-white">
<div class="flex flex-col items-center">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Auto Path</h3>
<div class="relative w-full overflow-hidden" style="max-height: 70vh;">
<img id="modalAutoPathImage"
class="w-full rounded-lg object-contain"
alt="Auto Path">
</div>
<!-- Add auto notes section -->
<div class="w-full mt-4">
<h4 class="text-md font-medium text-gray-700 mb-2">Auto Notes</h4>
<p id="modalAutoNotes" class="text-gray-600 bg-gray-50 p-3 rounded-lg min-h-[60px]">
<!-- Auto notes will be inserted here -->
</p>
</div>
<button onclick="closeAutoPathModal()"
class="mt-4 w-full sm:w-auto px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
Close
</button>
</div>
</div>
</div>

<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">Team Data</h1>
@@ -98,30 +123,33 @@ <h2 class="text-xl font-semibold mb-4 bg-gray-100 rounded px-4 py-2">
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Team #
</th>
<th class=" sm:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Alliance
</th>
<th class="sm:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Match
</th>
<th class=" md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Auto
<th class="md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Coral (Level 1-4)
</th>
<th class=" md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Teleop
<th class="md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Algae (Net/Processor/Human Player)
</th>
<th class=" md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Endgame
<th class="md:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Climb
</th>
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Auto Path
</th>
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Total
Defense
</th>
<th class=" lg:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="lg:table-cell px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Notes
</th>
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Scouter
</th>
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Alliance
</th>
<th class="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
@@ -136,16 +164,52 @@ <h2 class="text-xl font-semibold mb-4 bg-gray-100 rounded px-4 py-2">
data-scouter="{{ data.scouter_name }}">
<!-- In the team number cell -->
<td class="px-3 sm:px-6 py-4">
<a href="{{ url_for('scouting.team_view', team_number=data.team_number) }}"
class="text-blue-600 hover:text-blue-900">
<a href="{{ url_for('scouting.view_team', team_number=data.team_number) }}"
class="text-blue-600 hover:text-blue-900">
{{ data.team_number }}
</a>
</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="px-2 py-1 text-sm rounded-full
{% if data.alliance == 'red' %}
bg-red-100 text-red-800
{% else %}
bg-blue-100 text-blue-800
{% endif %}">
{{ data.alliance|title }}
</span>
</div>
</td>
<td class="sm:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">{{ data.match_number }}</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">{{ data.auto_points }}</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">{{ data.teleop_points }}</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">{{ data.endgame_points }}</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">{{ data.total_points }}</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">
{{ data.coral_level1 }}/{{ data.coral_level2 }}/{{ data.coral_level3 }}/{{ data.coral_level4 }}
</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">
{{ data.algae_net }}/{{ data.algae_processor }}/{{ data.human_player }}
<span class="text-xs text-gray-500"></span>
</td>
<td class="md:table-cell px-3 sm:px-6 py-4 whitespace-nowrap">
{% if data.climb_success %}
<span class="text-green-600">✓ {{ data.climb_type }}</span>
{% else %}
<span class="text-red-600">✗ {{ data.climb_type }}</span>
{% endif %}
</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
{% if data.auto_path %}
<button onclick="showAutoPath('{{ data.auto_path }}', '{{ data.auto_notes }}')"
class="text-blue-600 hover:text-blue-900">
<span class="hidden sm:inline">View Path</span>
<span class="sm:hidden">🗺️</span>
</button>
{% else %}
<span class="text-gray-400">No path</span>
{% endif %}
</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
{{ data.defense_rating }}/5
</td>
<td class="lg:table-cell px-3 sm:px-6 py-4 whitespace-normal max-w-xs truncate">{{ data.notes }}</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
<div class="flex items-center space-x-2">
@@ -161,18 +225,6 @@ <h2 class="text-xl font-semibold mb-4 bg-gray-100 rounded px-4 py-2">
</div>
</div>
</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="px-2 py-1 text-sm rounded-full
{% if data.alliance == 'red' %}
bg-red-100 text-red-800
{% else %}
bg-blue-100 text-blue-800
{% endif %}">
{{ data.alliance|title }}
</span>
</div>
</td>
<td class="px-3 sm:px-6 py-4 whitespace-nowrap">
<div class="flex space-x-2">
{% if data.scouter_id|string == current_user.id|string %}
@@ -245,5 +297,33 @@ <h2 class="text-xl font-semibold mb-4 bg-gray-100 rounded px-4 py-2">
searchInput.addEventListener('input', filterRows);
filterType.addEventListener('change', filterRows);
});

function showAutoPath(pathData, autoNotes = '') {
const modal = document.getElementById('autoPathModal');
const image = document.getElementById('modalAutoPathImage');
const notes = document.getElementById('modalAutoNotes');

image.src = pathData;
notes.textContent = autoNotes || 'No auto notes provided';
modal.classList.remove('hidden');
}

function closeAutoPathModal() {
document.getElementById('autoPathModal').classList.add('hidden');
}

// Close modal when clicking outside
document.getElementById('autoPathModal').addEventListener('click', function(e) {
if (e.target === this) {
closeAutoPathModal();
}
});

// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeAutoPathModal();
}
});
</script>
{% endblock %}
41 changes: 30 additions & 11 deletions app/templates/scouting/matches.html
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ <h1 class="text-2xl font-bold mb-2">Match List</h1>
Blue Alliance
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Final Score
Details
</th>
</tr>
</thead>
@@ -35,36 +35,55 @@ <h1 class="text-2xl font-bold mb-2">Match List</h1>
</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-col space-y-1">
<div class="flex flex-col space-y-2">
{% for team in match.red_teams %}
<div class="text-sm">
<a href="{{ url_for('team.view_team', team_number=team.number) }}"
class="text-red-600 hover:text-red-800">
class="text-red-600 hover:text-red-800 font-medium">
{{ team.number }}
</a>
<span class="text-gray-500">- {{ team.total_points }} points</span>
<div class="text-xs text-gray-500">
Coral: {{ team.coral_total }} |
Algae: {{ team.algae_total }} |
{% if team.climb_success %}
<span class="text-green-600">✓ {{ team.climb_type }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-col space-y-1">
<div class="flex flex-col space-y-2">
{% for team in match.blue_teams %}
<div class="text-sm">
<a href="{{ url_for('team.view_team', team_number=team.number) }}"
class="text-blue-600 hover:text-blue-800">
class="text-blue-600 hover:text-blue-800 font-medium">
{{ team.number }}
</a>
<span class="text-gray-500">- {{ team.total_points }} points</span>
<div class="text-xs text-gray-500">
Coral: {{ team.coral_total }} |
Algae: {{ team.algae_total }} |
{% if team.climb_success %}
<span class="text-green-600">✓ {{ team.climb_type }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium">
<span class="text-red-600">{{ match.red_score }}</span>
-
<span class="text-blue-600">{{ match.blue_score }}</span>
<div class="text-sm">
<div class="font-medium">
<span class="text-red-600">{{ match.red_score }}</span>
-
<span class="text-blue-600">{{ match.blue_score }}</span>
</div>
<div class="text-xs text-gray-500 mt-1">
Red: {{ match.red_coral_total }} coral, {{ match.red_algae_total }} algae
<br>
Blue: {{ match.blue_coral_total }} coral, {{ match.blue_algae_total }} algae
</div>
</div>
</td>
</tr>
45 changes: 28 additions & 17 deletions app/templates/scouting/team.html
Original file line number Diff line number Diff line change
@@ -51,16 +51,16 @@ <h3 class="font-medium text-gray-700">Average Points</h3>
<p class="mt-1">{{ (stats.total_points / stats.matches_played) | round(1) }}</p>
</div>
<div>
<h3 class="font-medium text-gray-700">Avg Auto</h3>
<p class="mt-1">{{ stats.auto_points | round(1) }}</p>
<h3 class="font-medium text-gray-700">Avg Coral</h3>
<p class="mt-1">{{ stats.avg_coral | round(1) }}</p>
</div>
<div>
<h3 class="font-medium text-gray-700">Avg Teleop</h3>
<p class="mt-1">{{ stats.teleop_points | round(1) }}</p>
<h3 class="font-medium text-gray-700">Avg Algae</h3>
<p class="mt-1">{{ stats.avg_algae | round(1) }}</p>
</div>
<div>
<h3 class="font-medium text-gray-700">Avg Endgame</h3>
<p class="mt-1">{{ stats.endgame_points | round(1) }}</p>
<h3 class="font-medium text-gray-700">Climb Success</h3>
<p class="mt-1">{{ (stats.climb_success_rate * 100) | round(1) }}%</p>
</div>
</div>
</div>
@@ -80,9 +80,9 @@ <h2 class="text-xl font-semibold mb-4">Match History</h2>
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Match</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Auto</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Teleop</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Endgame</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Coral (L1-L4)</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Algae (Net/Proc)</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Climb</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
</tr>
@@ -91,9 +91,20 @@ <h2 class="text-xl font-semibold mb-4">Match History</h2>
{% for match in matches %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">{{ match.event_code }} - {{ match.match_number }}</td>
<td class="px-6 py-4">{{ match.auto_points }}</td>
<td class="px-6 py-4">{{ match.teleop_points }}</td>
<td class="px-6 py-4">{{ match.endgame_points }}</td>
<td class="px-6 py-4">
{{ match.coral_level1 }}/{{ match.coral_level2 }}/{{ match.coral_level3 }}/{{ match.coral_level4 }}
</td>
<td class="px-6 py-4">
{{ match.algae_net }}/{{ match.algae_processor }}
{% if match.human_player %}<span class="text-green-600">✓ HP</span>{% endif %}
</td>
<td class="px-6 py-4">
{% if match.climb_success %}
<span class="text-green-600">✓ {{ match.climb_type }}</span>
{% else %}
<span class="text-red-600"></span>
{% endif %}
</td>
<td class="px-6 py-4">{{ match.total_points }}</td>
<td class="px-6 py-4">{{ match.notes }}</td>
</tr>
@@ -192,14 +203,14 @@ <h2 class="text-xl font-semibold mb-4">Match History</h2>
const consistency = calculateConsistency(matchesData);

const radarData = {
labels: ['Auto', 'Teleop', 'Endgame', 'Consistency', 'Overall'],
labels: ['Coral', 'Algae', 'Climb', 'Consistency', 'Defense'],
datasets: [{
data: [
{{ stats.auto_points|float }},
{{ stats.teleop_points|float }},
{{ stats.endgame_points|float }},
{{ stats.avg_coral|float }},
{{ stats.avg_algae|float }},
{{ (stats.climb_success_rate * 100)|float }},
consistency,
{{ (stats.total_points / stats.matches_played)|float }}
{{ stats.avg_defense|float }}
],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgb(54, 162, 235)',

0 comments on commit 48e60ec

Please sign in to comment.