Files
ShooterHub/frontend/sessions.html
2026-04-02 11:24:30 +02:00

463 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sessions ShooterHub</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/app.css">
<style>
.session-item { cursor: pointer; border-radius: 6px; padding: .5rem .75rem; }
.session-item:hover { background: #f0f4ff; }
.session-item.active { background: #0d6efd; color: #fff; }
.session-item.active .text-muted { color: rgba(255,255,255,.7) !important; }
#rightPanel { min-height: 60vh; }
.stage-card { border-left: 3px solid #dee2e6; }
.stage-card.has-results { border-left-color: #198754; }
.phase-label { font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.corr-field { max-width: 90px; }
.photo-thumb { width: 80px; height: 60px; object-fit: cover; border-radius: 4px; cursor: pointer; }
.btn-xs { font-size: .75rem; padding: .1rem .35rem; }
</style>
</head>
<body>
<div id="navbar"></div>
<div class="container-fluid py-4">
<div class="container mb-3 d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h2 class="fw-bold mb-0"><i class="bi bi-activity me-2"></i><span data-i18n="sessions.title">Shooting Sessions</span></h2>
<p class="text-muted small mb-0" data-i18n="sessions.subtitle">Prepare and record PRS, free practice, or speed shooting sessions.</p>
</div>
<button class="btn btn-primary" id="newSessionBtn">
<i class="bi bi-plus-lg me-1"></i><span data-i18n="sessions.new">New Session</span>
</button>
</div>
<!-- Type tabs -->
<div class="container mb-3">
<ul class="nav nav-tabs" id="sessionTabs">
<li class="nav-item">
<a class="nav-link active" href="#" data-type="prs">
<i class="bi bi-trophy me-1"></i><span data-i18n="sessions.tab.prs">PRS Match</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-type="free-practice">
<i class="bi bi-bullseye me-1"></i><span data-i18n="sessions.tab.fp">Free Practice</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-type="speed-shooting">
<i class="bi bi-lightning me-1"></i><span data-i18n="sessions.tab.speed">Speed Shooting</span>
</a>
</li>
</ul>
</div>
<div class="row g-0" style="min-height: 70vh;">
<!-- ── Left: session list ─────────────────────────────────────────────── -->
<div class="col-md-3 border-end px-3">
<div id="listSpinner" class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
<div id="listEmpty" class="text-muted small d-none" data-i18n="sessions.empty">No sessions yet.</div>
<div id="sessionsList"></div>
</div>
<!-- ── Right: session detail ──────────────────────────────────────────── -->
<div class="col-md-9 px-4" id="rightPanel">
<div class="text-center text-muted py-5" id="noSessionMsg">
<i class="bi bi-arrow-left fs-3 d-block mb-2"></i>
<span data-i18n="sessions.none">Select a session or create a new one</span>
</div>
<div id="sessionDetail" class="d-none">
<!-- Header -->
<div class="d-flex justify-content-between align-items-start mb-3 flex-wrap gap-2">
<div>
<h4 class="fw-bold mb-0" id="detailTitle"></h4>
<span class="text-muted small" id="detailMeta"></span>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" id="togglePublicBtn" title="Make public/private">
<i class="bi bi-globe me-1" id="togglePublicIcon"></i><span id="togglePublicLabel">Public</span>
</button>
<button class="btn btn-sm btn-outline-danger" id="deleteSessionBtn">
<i class="bi bi-trash me-1"></i><span data-i18n="btn.delete">Delete</span>
</button>
</div>
</div>
<!-- Session meta card -->
<div class="card border-0 shadow-sm mb-3" id="metaCard">
<div class="card-body">
<div class="row g-2 small" id="metaFields"></div>
</div>
</div>
<!-- Weather (collapsible) -->
<div class="mb-3">
<a class="text-decoration-none text-secondary small fw-semibold" data-bs-toggle="collapse" href="#weatherCollapse">
<i class="bi bi-cloud-sun me-1"></i><span data-i18n="sessions.weather.title">Weather conditions</span> <i class="bi bi-chevron-down"></i>
</a>
<div class="collapse mt-2" id="weatherCollapse">
<div class="card border-0 bg-light">
<div class="card-body">
<div class="row g-2" id="weatherFields"></div>
<button class="btn btn-sm btn-outline-primary mt-2" id="saveWeatherBtn">
<i class="bi bi-floppy me-1"></i><span data-i18n="sessions.weather.save">Save weather</span>
</button>
</div>
</div>
</div>
</div>
<!-- Linked analysis -->
<div class="mb-3">
<h6 class="fw-semibold text-secondary small mb-2">
<i class="bi bi-speedometer2 me-1"></i><span data-i18n="sessions.analysis.title">Linked analysis</span>
</h6>
<div class="card border-0 bg-light">
<div class="card-body" id="analysisBody">
<!-- filled by renderAnalysisSection() -->
</div>
</div>
</div>
<!-- PRS stages section -->
<div id="stagesSection" class="d-none">
<div class="d-flex align-items-center justify-content-between mb-3">
<h5 class="fw-semibold mb-0"><i class="bi bi-list-ol me-2"></i><span data-i18n="sessions.stages.title">Stages</span></h5>
<button class="btn btn-sm btn-outline-primary" id="addStageBtn">
<i class="bi bi-plus-lg me-1"></i><span data-i18n="sessions.stages.add">Add Stage</span>
</button>
</div>
<div id="stagesList"></div>
</div>
<!-- Free practice / speed shooting summary -->
<div id="genericDetail"></div>
</div>
</div>
</div>
</div>
<!-- ── Create Session Modal ──────────────────────────────────────────────── -->
<div class="modal fade" id="createModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createModalTitle"><i class="bi bi-plus-lg me-2"></i><span data-i18n="sessions.new">New Session</span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold" data-i18n="sessions.create.name">Session name</label>
<input type="text" class="form-control" id="createName" placeholder="e.g. Range day 308">
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.date">Date</label>
<input type="date" class="form-control" id="createDate">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.location">Location</label>
<input type="text" class="form-control" id="createLocation" placeholder="Range name">
</div>
</div>
<!-- PRS-specific fields -->
<div id="prsFields" class="d-none">
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.competition">Competition name</label>
<input type="text" class="form-control" id="createCompetition" placeholder="e.g. PRS Open Bretagne">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.category">Category</label>
<input type="text" class="form-control" id="createCategory" placeholder="e.g. Production">
</div>
</div>
</div>
<!-- Free practice fields -->
<div id="freePracticeFields" class="d-none">
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.distance">Distance (m)</label>
<input type="number" class="form-control" id="createDistance" min="1" placeholder="100">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.create.target">Target description</label>
<input type="text" class="form-control" id="createTarget" placeholder="e.g. A4 paper">
</div>
</div>
</div>
<!-- Speed shooting fields -->
<div id="speedFields" class="d-none">
<div class="mb-3">
<label class="form-label fw-semibold" data-i18n="sessions.create.format">Format</label>
<input type="text" class="form-control" id="createFormat" placeholder="e.g. IPSC, IDPA">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-i18n="sessions.create.rig">Rig</label>
<select class="form-select" id="createRig">
<option value="">— None —</option>
</select>
</div>
<div class="mb-2">
<label class="form-label fw-semibold" data-i18n="sessions.create.ammo">Ammo</label>
<div class="btn-group btn-group-sm w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="ammoType" id="ammoNone" value="none" checked>
<label class="btn btn-outline-secondary" for="ammoNone" data-i18n="sessions.create.ammo.none">None</label>
<input type="radio" class="btn-check" name="ammoType" id="ammoFactory" value="factory">
<label class="btn btn-outline-secondary" for="ammoFactory" data-i18n="sessions.create.ammo.factory">Factory</label>
<input type="radio" class="btn-check" name="ammoType" id="ammoReload" value="reload">
<label class="btn btn-outline-secondary" for="ammoReload" data-i18n="sessions.create.ammo.reload">Reloaded batch</label>
</div>
<select class="form-select d-none" id="createAmmo"></select>
<select class="form-select d-none" id="createBatch"></select>
<!-- Suggest ammo link (shown when factory is selected) -->
<div id="suggestAmmoWrap" class="d-none mt-2">
<a href="#" id="toggleSuggestAmmo" class="small text-primary">
<i class="bi bi-plus-circle me-1"></i><span data-i18n="sessions.create.ammo.suggest">Can't find your ammo? Suggest it</span>
</a>
<div id="suggestAmmoForm" class="d-none mt-2 p-3 border rounded bg-light small">
<p class="text-muted mb-2" data-i18n="sessions.create.ammo.suggest.note">Your suggestion will be visible to all once an admin verifies it.</p>
<div class="row g-2 mb-2">
<div class="col"><input type="text" class="form-control form-control-sm" id="saBrand" placeholder="Brand"></div>
<div class="col"><input type="text" class="form-control form-control-sm" id="saName" placeholder="Name / model"></div>
</div>
<div class="row g-2 mb-2">
<div class="col"><select class="form-select form-select-sm" id="saCaliber"><option value="">Loading…</option></select></div>
<div class="col"><input type="number" class="form-control form-control-sm" id="saBulletWeight" placeholder="Bullet weight (gr) *" step="0.1" min="0"></div>
</div>
<div class="mb-2">
<select class="form-select form-select-sm" id="saBulletType">
<option value="">Bullet type *</option>
<option value="FMJ">Full Metal Jacket (FMJ)</option>
<option value="HP">Hollow Point (HP)</option>
<option value="BTHP">Boat Tail Hollow Point (BTHP)</option>
<option value="SP">Soft Point (SP)</option>
<option value="HPBT">Hollow Point Boat Tail (HPBT)</option>
<option value="SMK">Sierra MatchKing (SMK)</option>
<option value="A_TIP">Hornady A-Tip</option>
<option value="MONO">Monolithic / Solid</option>
</select>
</div>
<div id="saAlert" class="alert alert-danger d-none py-1 small mb-2"></div>
<button class="btn btn-sm btn-primary" id="submitSuggestAmmo">
<i class="bi bi-send me-1"></i>Submit suggestion
</button>
</div>
</div>
</div>
<!-- Analysis -->
<div class="mb-2">
<label class="form-label fw-semibold">Chronograph analysis</label>
<div class="btn-group btn-group-sm w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="analysisMode" id="analysisModeNone" value="none" checked>
<label class="btn btn-outline-secondary" for="analysisModeNone">None</label>
<input type="radio" class="btn-check" name="analysisMode" id="analysisModeLink" value="link">
<label class="btn btn-outline-secondary" for="analysisModeLink">Link existing</label>
<input type="radio" class="btn-check" name="analysisMode" id="analysisModeUpload" value="upload">
<label class="btn btn-outline-secondary" for="analysisModeUpload"><i class="bi bi-upload me-1"></i>Upload CSV</label>
</div>
<!-- Link existing -->
<select class="form-select d-none" id="createAnalysisPicker"></select>
<!-- Upload new -->
<div id="createAnalysisUpload" class="d-none border rounded p-3 bg-light small">
<div class="mb-2">
<label class="form-label mb-1">CSV file <span class="text-danger">*</span></label>
<input type="file" class="form-control form-control-sm" id="createAnalysisCsv" accept=".csv,.txt">
</div>
<div class="mb-2">
<label class="form-label mb-1">Analysis name <span class="text-muted fw-normal">(optional — defaults to session name)</span></label>
<input type="text" class="form-control form-control-sm" id="createAnalysisName" placeholder="e.g. 308 match">
</div>
<div class="row g-2 mb-2">
<div class="col">
<label class="form-label mb-1">Chronograph model</label>
<select class="form-select form-select-sm" id="createAnalysisChronoType">
<option value="garmin_xero_c1_pro">Garmin Xero C1 Pro</option>
</select>
</div>
<div class="col">
<label class="form-label mb-1">Velocity unit</label>
<div class="btn-group btn-group-sm w-100" role="group">
<input type="radio" class="btn-check" name="createAnalysisVelUnit" id="caVelFps" value="fps" checked>
<label class="btn btn-outline-secondary" for="caVelFps">fps</label>
<input type="radio" class="btn-check" name="createAnalysisVelUnit" id="caVelMps" value="mps">
<label class="btn btn-outline-secondary" for="caVelMps">m/s</label>
</div>
</div>
</div>
</div>
</div>
<div id="createAlert" class="alert alert-danger d-none small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="btn.cancel">Cancel</button>
<button type="button" class="btn btn-primary" id="createSubmitBtn">
<span id="createSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
<span data-i18n="btn.submit">Create</span>
</button>
</div>
</div>
</div>
</div>
<!-- ── Add Stage Modal (PRS) ─────────────────────────────────────────────── -->
<div class="modal fade" id="addStageModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-lg me-2"></i><span data-i18n="sessions.stage.modal.title">Add Stage</span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.order">Stage #</label>
<input type="number" class="form-control" id="stageOrder" min="1" value="1">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.position">Position</label>
<select class="form-select" id="stagePosition">
<option value="PRONE">Prone</option>
<option value="STANDING">Standing</option>
<option value="SITTING">Sitting</option>
<option value="KNEELING">Kneeling</option>
<option value="BARRICADE">Barricade</option>
<option value="UNSUPPORTED">Unsupported</option>
<option value="OTHER">Other</option>
</select>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.distance">Distance (m)</label>
<input type="number" class="form-control" id="stageDistance" min="1" placeholder="300">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.maxtime">Max time (s)</label>
<input type="number" class="form-control" id="stageMaxTime" min="1" placeholder="90">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.shots">Shots</label>
<input type="number" class="form-control" id="stageShots" min="1" value="1">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.tw">Target W (cm)</label>
<input type="number" class="form-control" id="stageTargetW" step="0.1" placeholder="20">
</div>
<div class="col">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.th">Target H (cm)</label>
<input type="number" class="form-control" id="stageTargetH" step="0.1" placeholder="30">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-i18n="sessions.stage.modal.notes">Prep notes</label>
<textarea class="form-control" id="stageNotePrep" rows="2" placeholder="Wind call, dope…"></textarea>
</div>
<div id="stageAlert" class="alert alert-danger d-none small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="btn.cancel">Cancel</button>
<button type="button" class="btn btn-primary" id="stageSubmitBtn" data-i18n="sessions.stages.add">Add Stage</button>
</div>
</div>
</div>
</div>
<!-- ── Session Photo Upload Modal ───────────────────────────────────────── -->
<div class="modal fade" id="sessionPhotoModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-image me-2"></i>Add photo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">Photo</label>
<input type="file" class="form-control" id="sessionPhotoFileInput" accept="image/jpeg,image/png,image/webp">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Caption <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" id="sessionPhotoCaption" placeholder="e.g. 100m cold bore">
</div>
<div id="sessionPhotoAlert" class="alert alert-danger d-none small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="sessionPhotoSubmitBtn">
<span id="sessionPhotoSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
Upload
</button>
</div>
</div>
</div>
</div>
<!-- ── Group Photo Modal ─────────────────────────────────────────────────── -->
<div class="modal fade" id="groupPhotoModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark border-0">
<div class="modal-header border-0 py-2 px-3">
<span class="text-white fw-semibold" id="gpModalTitle"></span>
<div class="d-flex gap-2 ms-auto me-2">
<a id="gpModalMeasureBtn" href="#" class="btn btn-sm btn-outline-light">
<i class="bi bi-crosshair2 me-1"></i>Measure
</a>
<a id="gpModalOpenBtn" href="#" target="_blank" class="btn btn-sm btn-outline-light">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<!-- Stats bar + unit switch -->
<div class="px-3 py-2 border-top border-secondary d-flex flex-wrap align-items-center gap-2">
<div id="gpModalStats" class="small text-white-50 d-flex flex-wrap gap-3 flex-grow-1"></div>
<div class="btn-group btn-group-sm ms-auto flex-shrink-0" id="gpModalUnitBtns">
<button class="btn btn-outline-light" data-dist-unit="mm">mm</button>
<button class="btn btn-outline-light" data-dist-unit="moa">MOA</button>
<button class="btn btn-outline-light" data-dist-unit="mrad">MRAD</button>
</div>
</div>
<!-- Photo -->
<div class="text-center" style="max-height:75vh;overflow:hidden;background:#111">
<img id="gpModalImg" src="" alt="" style="max-width:100%;max-height:75vh;object-fit:contain">
</div>
</div>
</div>
</div>
<div id="toastContainer"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/api.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/calibers.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/sessions.js"></script>
</body>
</html>