more stuff to test

main
DustyP 9 months ago
parent baca5c4a2a
commit 03e62c48d7

@ -1054,32 +1054,27 @@ func (s *Server) handleRaceManage() http.HandlerFunc {
return return
} }
// Get heats for the group // Get heat results
heats, err := s.db.GetHeats(currentGroup.ID) results, err := s.db.GetHeatResults(currentGroup.ID)
if err != nil { if err != nil {
s.logger.Error("Failed to get heats", "error", err) s.logger.Error("Failed to get heat results", "error", err)
http.Error(w, "Failed to get heats", http.StatusInternalServerError) http.Error(w, "Failed to get heat results", http.StatusInternalServerError)
return return
} }
// Get racers for the group // Get heats for the group
racers, err := s.db.GetRacersByGroup(currentGroup.ID) heatData, err := s.db.GetHeatData(currentGroup.ID, currentHeatNum)
if err != nil { if err != nil {
s.logger.Error("Failed to get racers", "error", err) s.logger.Error("Failed to get heats", "error", err)
http.Error(w, "Failed to get racers", http.StatusInternalServerError) http.Error(w, "Failed to get heats", http.StatusInternalServerError)
return return
} }
// Get heat results // Get next heat data
results, err := s.db.GetHeatResults(currentGroup.ID) nextHeatData, _ := s.db.GetHeatData(currentGroup.ID, currentHeatNum+1)
if err != nil {
s.logger.Error("Failed to get heat results", "error", err)
http.Error(w, "Failed to get heat results", http.StatusInternalServerError)
return
}
// Render template // Render template
component := templates.RaceManage(groups, currentGroup, heats, racers, currentHeatNum, results) component := templates.RaceManage(heatData, nextHeatData, groups, results)
if err := component.Render(r.Context(), w); err != nil { if err := component.Render(r.Context(), w); err != nil {
s.logger.Error("Failed to render race manage template", "error", err) s.logger.Error("Failed to render race manage template", "error", err)
http.Error(w, "Failed to render page", http.StatusInternalServerError) http.Error(w, "Failed to render page", http.StatusInternalServerError)

@ -7,7 +7,7 @@ import (
) )
// RaceManage renders the race management view // RaceManage renders the race management view
templ RaceManage(groups []models.Group, currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) { templ RaceManage(heatData *models.HeatData, nextHeat *models.HeatData, groups []models.Group, results []models.HeatResult) {
@Layout("Race Management") { @Layout("Race Management") {
<div class="container-fluid mt-3"> <div class="container-fluid mt-3">
<div class="row mb-4"> <div class="row mb-4">
@ -21,7 +21,7 @@ templ RaceManage(groups []models.Group, currentGroup models.Group, heats []model
<label for="group-select" class="form-label">Racing Group</label> <label for="group-select" class="form-label">Racing Group</label>
<select id="group-select" class="form-select" hx-post="/api/race/set-group" hx-trigger="change" hx-swap="none"> <select id="group-select" class="form-select" hx-post="/api/race/set-group" hx-trigger="change" hx-swap="none">
for _, group := range groups { for _, group := range groups {
<option value={ strconv.FormatInt(group.ID, 10) } selected?={ group.ID == currentGroup.ID }> <option value={ strconv.FormatInt(group.ID, 10) } selected?={ group.ID == heatData.Group.ID }>
{ group.Name } { group.Name }
</option> </option>
} }
@ -29,12 +29,12 @@ templ RaceManage(groups []models.Group, currentGroup models.Group, heats []model
</div> </div>
<div class="mb-3"> <div class="mb-3">
<h5>Current Heat: { strconv.Itoa(currentHeatNum) } of { strconv.Itoa(len(heats)) }</h5> <h5>Current Heat: { strconv.Itoa(heatData.HeatNumber) } of { strconv.Itoa(heatData.TotalHeats) }</h5>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="prev-heat-btn" class="btn btn-secondary" hx-post="/api/race/previous-heat" hx-swap="none" disabled?={ currentHeatNum <= 1 }> <button id="prev-heat-btn" class="btn btn-secondary" hx-post="/api/race/previous-heat" hx-swap="none" disabled?={ heatData.HeatNumber <= 1 }>
<i class="bi bi-arrow-left"></i> Previous Heat <i class="bi bi-arrow-left"></i> Previous Heat
</button> </button>
<button id="next-heat-btn" class="btn btn-secondary" hx-post="/api/race/next-heat" hx-swap="none" disabled?={ currentHeatNum >= len(heats) }> <button id="next-heat-btn" class="btn btn-secondary" hx-post="/api/race/next-heat" hx-swap="none" disabled?={ heatData.HeatNumber >= heatData.TotalHeats }>
Next Heat <i class="bi bi-arrow-right"></i> Next Heat <i class="bi bi-arrow-right"></i>
</button> </button>
</div> </div>
@ -102,17 +102,16 @@ templ RaceManage(groups []models.Group, currentGroup models.Group, heats []model
</div> </div>
<!-- Current Heat Display --> <!-- Current Heat Display -->
@currentHeatDisplay(heats, racers, currentHeatNum, results) @currentHeatDisplay(heatData)
<!-- Next Heat Preview --> <!-- Next Heat Preview -->
if currentHeatNum < len(heats) { if nextHeat != nil {
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header bg-secondary text-white"> <div class="card-header bg-secondary text-white">
<h4 class="mb-0">Next Heat: { strconv.Itoa(currentHeatNum + 1) }</h4> <h4 class="mb-0">Next Heat: { strconv.Itoa(nextHeat.HeatNumber) }</h4>
</div> </div>
<div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Lane</th> <th>Lane</th>
@ -121,254 +120,46 @@ templ RaceManage(groups []models.Group, currentGroup models.Group, heats []model
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
for _, heat := range heats { if nextHeat.Lane1 != nil {
if heat.HeatNum == currentHeatNum + 1 { @raceNextHeatRow(*nextHeat.Lane1)
if heat.Lane1ID != nil {
@nextHeatRacer(1, *heat.Lane1ID, racers)
}
if heat.Lane2ID != nil {
@nextHeatRacer(2, *heat.Lane2ID, racers)
} }
if heat.Lane3ID != nil { if nextHeat.Lane2 != nil {
@nextHeatRacer(3, *heat.Lane3ID, racers) @raceNextHeatRow(*nextHeat.Lane2)
}
if heat.Lane4ID != nil {
@nextHeatRacer(4, *heat.Lane4ID, racers)
} }
if nextHeat.Lane3 != nil {
@raceNextHeatRow(*nextHeat.Lane3)
} }
if nextHeat.Lane4 != nil {
@raceNextHeatRow(*nextHeat.Lane4)
} }
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div>
} }
</div> </div>
<script>
// Set up SSE connection
console.log("Setting up SSE connection...");
const eventSource = new EventSource('/api/events');
eventSource.onopen = function() {
console.log("SSE connection opened");
};
eventSource.onerror = function(error) {
console.error("SSE connection error:", error);
};
eventSource.addEventListener('debug', function(event) {
console.log("Debug event received:", event.data);
});
eventSource.addEventListener('lane-finish', function(event) {
console.log("Lane finish event received:", event.data);
try {
const laneFinishData = JSON.parse(event.data);
const laneTimeElement = document.getElementById(`lane-${laneFinishData.lane}-time`);
if (laneTimeElement) {
laneTimeElement.textContent = laneFinishData.time.toFixed(4);
}
const lanePositionElement = document.getElementById(`lane-${laneFinishData.lane}-position`);
if (lanePositionElement) {
lanePositionElement.textContent = laneFinishData.place;
}
} catch (error) {
console.error("Error processing lane finish event:", error);
}
});
eventSource.addEventListener('status', function(event) {
console.log("Status event received:", event.data);
try {
const statusData = JSON.parse(event.data);
let statusText = 'Unknown';
let statusClass = 'bg-secondary';
if (statusData.status === 'idle') {
statusText = 'Ready';
statusClass = 'bg-primary';
// Reset all times and positions
document.querySelectorAll('[id^="lane-"][id$="-time"]').forEach(el => {
el.textContent = '--.-';
});
document.querySelectorAll('[id^="lane-"][id$="-position"]').forEach(el => {
el.textContent = '-';
});
} else if (statusData.status === 'running') {
statusText = 'Race Running';
statusClass = 'bg-success';
} else if (statusData.status === 'finished') {
statusText = 'Race Complete';
statusClass = 'bg-info';
// Save heat results
saveHeatResults();
}
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.textContent = statusText;
statusIndicator.className = `badge mb-3 ${statusClass}`;
}
const timerDisplay = document.getElementById('timer');
if (timerDisplay && statusData.status === 'idle') {
timerDisplay.textContent = '0.000';
}
} catch (error) {
console.error("Error processing status event:", error);
}
});
// Admin socket for admin-specific events
const adminSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/admin`);
adminSocket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data === 'heat-changed' || data === 'group-changed' || data === 'heat-rerun') {
// Reload the page when heat or group changes
window.location.reload();
}
};
// Add event listener for admin events
eventSource.addEventListener('admin', function(event) {
console.log("Admin event received:", event.data);
try {
const adminData = JSON.parse(event.data);
if (adminData.event === 'heat-changed' ||
adminData.event === 'group-changed' ||
adminData.event === 'heat-rerun') {
// Reload the page when heat or group changes
window.location.reload();
}
} catch (error) {
console.error("Error processing admin event:", error);
}
});
// Function to reset the timer
function resetTimer() {
fetch('/api/reset', { method: 'POST' })
.then(response => {
if (!response.ok) {
console.error('Failed to reset timer');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Function to force end the current heat
function forceEndHeat() {
fetch('/api/force-end', { method: 'POST' })
.then(response => {
if (!response.ok) {
console.error('Failed to force end heat');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Function to save heat results
function saveHeatResults() {
// Get lane times and positions from the UI
const lanes = [1, 2, 3, 4];
const results = {
group_id: parseInt(document.getElementById('group-select').value),
heat_number: parseInt('{ strconv.Itoa(currentHeatNum) }'),
};
lanes.forEach(lane => {
const timeElement = document.getElementById(`lane-${lane}-time`);
const positionElement = document.getElementById(`lane-${lane}-position`);
if (timeElement && positionElement) {
const timeText = timeElement.textContent.trim();
const positionText = positionElement.textContent.trim();
results[`lane${lane}_time`] = timeText !== '--.-' ? parseFloat(timeText) : 0;
results[`lane${lane}_position`] = positionText !== '-' ? parseInt(positionText) : 0;
}
});
// Save results to the server
fetch('/api/race/save-result', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(results),
})
.then(response => {
if (!response.ok) {
console.error('Failed to save heat results');
}
})
.catch(error => {
console.error('Error:', error);
});
}
function getOrdinal(n) {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v-20)%10] || s[v] || s[0]);
}
</script>
} }
} }
// Find the current heat // Find the current heat
templ currentHeatDisplay(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) { templ currentHeatDisplay(heatData *models.HeatData) {
{{
// Find the current heat
var currentHeat models.Heat
for _, heat := range heats {
if heat.HeatNum == currentHeatNum {
currentHeat = heat
break
}
}
// Find the current result
var currentResult *models.HeatResult
for _, result := range results {
if result.HeatNumber == currentHeatNum {
currentResult = &result
break
}
}
}}
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h4 class="mb-0">Current Heat: { strconv.Itoa(currentHeatNum) }</h4> <h4 class="mb-0">Current Heat: { strconv.Itoa(heatData.HeatNumber) }</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
if currentHeat.Lane1ID != nil { if heatData.Lane1 != nil {
@raceLaneInfo(1, *currentHeat.Lane1ID, racers, currentResult) @raceLaneInfo(*heatData.Lane1)
} }
if currentHeat.Lane2ID != nil { if heatData.Lane2 != nil {
@raceLaneInfo(2, *currentHeat.Lane2ID, racers, currentResult) @raceLaneInfo(*heatData.Lane2)
} }
if currentHeat.Lane3ID != nil { if heatData.Lane3 != nil {
@raceLaneInfo(3, *currentHeat.Lane3ID, racers, currentResult) @raceLaneInfo(*heatData.Lane3)
} }
if currentHeat.Lane4ID != nil { if heatData.Lane4 != nil {
@raceLaneInfo(4, *currentHeat.Lane4ID, racers, currentResult) @raceLaneInfo(*heatData.Lane4)
} }
</div> </div>
</div> </div>
@ -376,51 +167,21 @@ templ currentHeatDisplay(heats []models.Heat, racers []models.Racer, currentHeat
} }
// Helper template for displaying a lane in the race manager // Helper template for displaying a lane in the race manager
templ raceLaneInfo(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) { templ raceLaneInfo(laneData models.LaneData) {
{{
// Find the racer
var racer models.Racer
for _, r := range racers {
if r.ID == racerID {
racer = r
break
}
}
// Get time and position from result if available
var timeStr string = "--.-"
var positionStr string = "-"
if result != nil {
if lane == 1 && result.Lane1Time > 0 {
timeStr = fmt.Sprintf("%.4f", result.Lane1Time)
positionStr = strconv.Itoa(result.Lane1Position)
} else if lane == 2 && result.Lane2Time > 0 {
timeStr = fmt.Sprintf("%.4f", result.Lane2Time)
positionStr = strconv.Itoa(result.Lane2Position)
} else if lane == 3 && result.Lane3Time > 0 {
timeStr = fmt.Sprintf("%.4f", result.Lane3Time)
positionStr = strconv.Itoa(result.Lane3Position)
} else if lane == 4 && result.Lane4Time > 0 {
timeStr = fmt.Sprintf("%.4f", result.Lane4Time)
positionStr = strconv.Itoa(result.Lane4Position)
}
}
}}
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<div class="card h-100"> <div class="card h-100">
<div class="card-header bg-secondary text-white"> <div class="card-header bg-secondary text-white">
<h5 class="mb-0">Lane { strconv.Itoa(lane) }</h5> <h5 class="mb-0">Lane { strconv.Itoa(laneData.Lane) }</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{ racer.FirstName } { racer.LastName }</h5> <h5 class="card-title">{ laneData.Name }</h5>
<p class="card-text">Car #: { racer.CarNumber }</p> <p class="card-text">Car #: { laneData.CarNum }</p>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div> <div>
<strong>Time:</strong> <span id="lane-{strconv.Itoa(lane)}-time">{ timeStr }</span> <strong>Time:</strong> <span>{ fmt.Sprintf("%.3f", laneData.Time) }</span>
</div> </div>
<div> <div>
<strong>Position:</strong> <span id="lane-{strconv.Itoa(lane)}-position">{ positionStr }</span> <strong>Position:</strong> <span>{ strconv.Itoa(laneData.Place) }</span>
</div> </div>
</div> </div>
</div> </div>
@ -429,20 +190,10 @@ templ raceLaneInfo(lane int, racerID int64, racers []models.Racer, result *model
} }
// Helper template for displaying a racer in the next heat // Helper template for displaying a racer in the next heat
templ nextHeatRacer(lane int, racerID int64, racers []models.Racer) { templ nextHeatRacer(laneData models.LaneData) {
{{
// Find racer
var racer models.Racer
for _, r := range racers {
if r.ID == racerID {
racer = r
break
}
}
}}
<tr> <tr>
<td>{ strconv.Itoa(lane) }</td> <td>{ strconv.Itoa(laneData.Lane) }</td>
<td>{ racer.FirstName } { racer.LastName }</td> <td>{ laneData.Name }</td>
<td>{ racer.CarNumber }</td> <td>{ laneData.CarNum }</td>
</tr> </tr>
} }

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save