233 lines
9.9 KiB
HTML
233 lines
9.9 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2>Bike Setups</h2>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSetupModal">
|
|
<i class="bi bi-plus-lg"></i> Add Setup
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle" id="setupsTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Frame</th>
|
|
<th>Chainring</th>
|
|
<th>Rear Cog</th>
|
|
<th>Gear Ratio</th>
|
|
<th>Rides</th>
|
|
<th>Distance</th>
|
|
<th>Weight</th>
|
|
<th>Active</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="setupsTableBody">
|
|
<!-- Populated by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Modal -->
|
|
<div class="modal fade" id="addSetupModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="setupModalLabel">Add Bike Setup</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="setupForm">
|
|
<input type="hidden" id="setupId" name="id">
|
|
<div class="mb-3">
|
|
<label for="name" class="form-label">Name (Optional)</label>
|
|
<input type="text" class="form-control" id="name" name="name" placeholder="e.g. Track Bike">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="frame" class="form-label">Frame</label>
|
|
<input type="text" class="form-control" id="frame" name="frame" required
|
|
placeholder="e.g. Dolan Pre Cursa">
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="chainring" class="form-label">Chainring</label>
|
|
<input type="number" class="form-control" id="chainring" name="chainring" required min="1"
|
|
placeholder="Teeth">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<input type="number" class="form-control" id="rearCog" name="rear_cog" required min="1"
|
|
placeholder="Teeth">
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label for="weightKg" class="form-label">Weight (kg)</label>
|
|
<input type="number" class="form-control" id="weightKg" name="weight_kg" step="0.1" min="0"
|
|
placeholder="e.g. 9.0">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label for="purchaseDate" class="form-label">Purchase Date</label>
|
|
<input type="date" class="form-control" id="purchaseDate" name="purchase_date">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label for="retirementDate" class="form-label">Retired Date</label>
|
|
<input type="date" class="form-control" id="retirementDate" name="retirement_date">
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveSetup()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let currentSetups = [];
|
|
const setupModal = new bootstrap.Modal(document.getElementById('addSetupModal'));
|
|
|
|
document.addEventListener('DOMContentLoaded', loadSetups);
|
|
|
|
async function loadSetups() {
|
|
try {
|
|
const response = await fetch('/api/bike-setups/');
|
|
if (!response.ok) throw new Error('Failed to load setups');
|
|
currentSetups = await response.json();
|
|
renderTable();
|
|
} catch (error) {
|
|
showToast(error.message, 'danger');
|
|
}
|
|
}
|
|
|
|
function renderTable() {
|
|
const tbody = document.getElementById('setupsTableBody');
|
|
tbody.innerHTML = '';
|
|
|
|
currentSetups.forEach(setup => {
|
|
const ratio = (setup.chainring / setup.rear_cog).toFixed(2);
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${setup.name || '-'}</td>
|
|
<td>${setup.frame}</td>
|
|
<td>${setup.chainring}t</td>
|
|
<td>${setup.rear_cog}t</td>
|
|
<td>${ratio}</td>
|
|
<td><span class="badge bg-secondary">${setup.activity_count || 0}</span></td>
|
|
<td>${setup.total_distance ? (setup.total_distance / 1000).toFixed(0) + ' km' : '-'}</td>
|
|
<td>${setup.weight_kg ? setup.weight_kg + 'kg' : '-'}</td>
|
|
<td>${setup.retirement_date ? '<span class="badge bg-danger">Retired</span>' : '<span class="badge bg-success">Active</span>'}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary me-1" onclick="editSetup(${setup.id})">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteSetup(${setup.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function resetForm() {
|
|
document.getElementById('setupForm').reset();
|
|
document.getElementById('setupId').value = '';
|
|
document.getElementById('setupModalLabel').textContent = 'Add Bike Setup';
|
|
}
|
|
|
|
// Hook into modal show event to reset form if adding
|
|
document.getElementById('addSetupModal').addEventListener('show.bs.modal', function (event) {
|
|
if (!event.relatedTarget || event.relatedTarget.getAttribute('data-bs-target')) {
|
|
// If triggered by button (not manual show for edit), reset
|
|
// Actually better to just check if we set an ID
|
|
}
|
|
});
|
|
|
|
// Reset on close
|
|
document.getElementById('addSetupModal').addEventListener('hidden.bs.modal', resetForm);
|
|
|
|
|
|
function editSetup(id) {
|
|
const setup = currentSetups.find(s => s.id === id);
|
|
if (!setup) return;
|
|
|
|
document.getElementById('setupId').value = setup.id;
|
|
document.getElementById('name').value = setup.name || '';
|
|
document.getElementById('frame').value = setup.frame;
|
|
document.getElementById('chainring').value = setup.chainring;
|
|
document.getElementById('rearCog').value = setup.rear_cog;
|
|
document.getElementById('weightKg').value = setup.weight_kg || '';
|
|
document.getElementById('purchaseDate').value = setup.purchase_date ? setup.purchase_date.split('T')[0] : '';
|
|
document.getElementById('retirementDate').value = setup.retirement_date ? setup.retirement_date.split('T')[0] : '';
|
|
|
|
document.getElementById('setupModalLabel').textContent = 'Edit Bike Setup';
|
|
setupModal.show();
|
|
}
|
|
|
|
async function saveSetup() {
|
|
const id = document.getElementById('setupId').value;
|
|
const data = {
|
|
name: document.getElementById('name').value || null,
|
|
frame: document.getElementById('frame').value,
|
|
chainring: parseInt(document.getElementById('chainring').value),
|
|
rear_cog: parseInt(document.getElementById('rearCog').value),
|
|
weight_kg: document.getElementById('weightKg').value ? parseFloat(document.getElementById('weightKg').value) : null,
|
|
purchase_date: document.getElementById('purchaseDate').value || null,
|
|
retirement_date: document.getElementById('retirementDate').value || null
|
|
};
|
|
|
|
if (!data.frame || !data.chainring || !data.rear_cog) {
|
|
showToast('Please fill in all required fields', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const method = id ? 'PUT' : 'POST';
|
|
const url = id ? `/api/bike-setups/${id}` : '/api/bike-setups/';
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to save setup');
|
|
|
|
showToast('Setup saved successfully', 'success');
|
|
setupModal.hide();
|
|
loadSetups();
|
|
} catch (error) {
|
|
showToast(error.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function deleteSetup(id) {
|
|
if (!confirm('Are you sure you want to delete this setup?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/bike-setups/${id}`, { method: 'DELETE' });
|
|
if (!response.ok) throw new Error('Failed to delete setup');
|
|
|
|
showToast('Setup deleted', 'success');
|
|
loadSetups();
|
|
} catch (error) {
|
|
showToast(error.message, 'danger');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |