219 lines
9.9 KiB
HTML
219 lines
9.9 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ session.label }} — The Shooter's Network{% endblock %}
|
|
{% block content %}
|
|
|
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
|
<div>
|
|
<div style="font-size:0.82rem;color:#888;margin-bottom:.2rem;">
|
|
{% if is_owner %}<a href="{{ url_for('sessions.index') }}">Sessions</a> › {% endif %}
|
|
{{ session.session_date.strftime('%d %b %Y') }}
|
|
{% if session.is_public %}
|
|
<span style="background:#e8f5e9;color:#27ae60;font-size:0.75rem;padding:.1rem .45rem;border-radius:3px;margin-left:.4rem;">Public</span>
|
|
{% endif %}
|
|
</div>
|
|
<h1 style="margin:0;">{{ session.label }}</h1>
|
|
<div style="font-size:0.88rem;color:#666;margin-top:.4rem;">
|
|
by {{ session.user.display_name or session.user.email.split('@')[0] }}
|
|
</div>
|
|
</div>
|
|
{% if is_owner %}
|
|
<div style="display:flex;gap:.75rem;">
|
|
<a href="{{ url_for('sessions.edit', session_id=session.id) }}"
|
|
style="background:#f0f4ff;color:#1a1a2e;padding:0.5rem 1.1rem;border-radius:4px;font-size:0.9rem;text-decoration:none;">
|
|
Edit
|
|
</a>
|
|
<form method="post" action="{{ url_for('sessions.delete', session_id=session.id) }}"
|
|
onsubmit="return confirm('Delete this session? This cannot be undone.');">
|
|
<button type="submit"
|
|
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ---- Stats cards ---- #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;margin-bottom:2rem;">
|
|
|
|
{% if session.location_name or session.distance_m %}
|
|
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Location</div>
|
|
{% if session.location_name %}<div style="font-weight:600;">{{ session.location_name }}</div>{% endif %}
|
|
{% if session.distance_m %}<div style="color:#555;font-size:0.9rem;">{{ session.distance_m }} m</div>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session.weather_cond or session.weather_temp_c is not none or session.weather_wind_kph is not none %}
|
|
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Weather</div>
|
|
{% if session.weather_cond %}<div style="font-weight:600;">{{ session.weather_cond.replace('_',' ').title() }}</div>{% endif %}
|
|
<div style="color:#555;font-size:0.9rem;">
|
|
{% if session.weather_temp_c is not none %}{{ session.weather_temp_c }}°C{% endif %}
|
|
{% if session.weather_wind_kph is not none %} {{ session.weather_wind_kph }} km/h wind{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session.rifle %}
|
|
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Rifle / Handgun</div>
|
|
<div style="font-weight:600;">{{ session.rifle.name }}</div>
|
|
{% if session.rifle.caliber %}<div style="color:#555;font-size:0.9rem;">{{ session.rifle.caliber }}</div>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session.scope %}
|
|
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Scope</div>
|
|
<div style="font-weight:600;">{{ session.scope.name }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session.ammo_brand or session.ammo_weight_gr is not none %}
|
|
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Ammo</div>
|
|
{% if session.ammo_brand %}<div style="font-weight:600;">{{ session.ammo_brand }}</div>{% endif %}
|
|
<div style="color:#555;font-size:0.9rem;">
|
|
{% if session.ammo_weight_gr is not none %}{{ session.ammo_weight_gr }} gr{% endif %}
|
|
{% if session.ammo_lot %} lot {{ session.ammo_lot }}{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
{% if session.notes %}
|
|
<h2>Notes</h2>
|
|
<p style="color:#555;white-space:pre-wrap;">{{ session.notes }}</p>
|
|
{% endif %}
|
|
|
|
{# ---- Photos ---- #}
|
|
{% if session.photos or is_owner %}
|
|
<h2>Photos</h2>
|
|
{% if session.photos %}
|
|
<div style="display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
|
{% for photo in session.photos %}
|
|
<div>
|
|
<div style="position:relative;display:inline-block;">
|
|
<img src="{{ photo.photo_url }}"
|
|
data-gallery="session-{{ session.id }}"
|
|
data-src="{{ photo.photo_url }}"
|
|
data-caption="{{ photo.caption or '' }}"
|
|
alt="{{ photo.caption or '' }}"
|
|
style="height:180px;width:auto;border-radius:6px;object-fit:cover;display:block;">
|
|
{% if is_owner %}
|
|
<form method="post"
|
|
action="{{ url_for('sessions.delete_photo', session_id=session.id, photo_id=photo.id) }}"
|
|
onsubmit="return confirm('Delete this photo?');"
|
|
style="position:absolute;top:4px;right:4px;">
|
|
<button type="submit"
|
|
style="background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:3px;padding:.2rem .45rem;font-size:0.8rem;cursor:pointer;line-height:1.2;">
|
|
✕
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% if photo.annotations and photo.annotations.stats %}
|
|
{% set s = photo.annotations.stats %}
|
|
<div style="font-size:0.78rem;background:#f0f4ff;color:#1a1a2e;padding:.2rem .45rem;border-radius:3px;margin-top:.3rem;font-weight:600;">
|
|
{{ s.shot_count }} shots · {{ '%.2f'|format(s.group_size_moa) }} MOA ES
|
|
</div>
|
|
{% endif %}
|
|
{% if photo.caption %}
|
|
<div style="font-size:0.78rem;color:#666;margin-top:.25rem;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
|
{{ photo.caption }}
|
|
</div>
|
|
{% endif %}
|
|
{% if is_owner %}
|
|
<div style="display:flex;gap:.35rem;margin-top:.35rem;">
|
|
{% for label, deg in [('↺', -90), ('↻', 90), ('180°', 180)] %}
|
|
<form method="post" action="{{ url_for('sessions.rotate_photo_view', session_id=session.id, photo_id=photo.id) }}">
|
|
<input type="hidden" name="degrees" value="{{ deg }}">
|
|
<button type="submit"
|
|
style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.2rem .55rem;font-size:0.8rem;cursor:pointer;">
|
|
{{ label }}
|
|
</button>
|
|
</form>
|
|
{% endfor %}
|
|
</div>
|
|
<a href="{{ url_for('sessions.annotate_photo', session_id=session.id, photo_id=photo.id) }}"
|
|
style="display:inline-block;margin-top:.4rem;padding:.3rem .75rem;border-radius:4px;font-size:0.82rem;text-decoration:none;
|
|
{% if photo.annotations and photo.annotations.stats %}
|
|
background:#e8f5e9;color:#27ae60;border:1px solid #a5d6a7;
|
|
{% else %}
|
|
background:#1a1a2e;color:#fff;border:1px solid #1a1a2e;
|
|
{% endif %}">
|
|
{% if photo.annotations and photo.annotations.stats %}✓{% else %}▶{% endif %}
|
|
Measure group
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if is_owner %}
|
|
<form method="post"
|
|
action="{{ url_for('sessions.upload_photo', session_id=session.id) }}"
|
|
enctype="multipart/form-data"
|
|
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
|
|
<div>
|
|
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Add photo</label>
|
|
<input type="file" name="photo" accept="image/*" required style="font-size:0.9rem;">
|
|
</div>
|
|
<div>
|
|
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Caption (optional)</label>
|
|
<input type="text" name="caption" placeholder="e.g. 300 m target"
|
|
style="padding:.45rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;">
|
|
</div>
|
|
<button type="submit"
|
|
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
|
Upload
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{# ---- Analyses ---- #}
|
|
<h2>Analyses{% if analyses %} ({{ analyses|length }}){% endif %}</h2>
|
|
|
|
{% if analyses %}
|
|
<table style="margin-bottom:1.5rem;">
|
|
<thead>
|
|
<tr><th>Title</th><th>Date</th><th>Shots</th><th>Groups</th><th>Mean speed</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for a in analyses %}
|
|
<tr style="cursor:pointer;" onclick="location.href='{{ url_for('analyses.detail', analysis_id=a.id) }}'">
|
|
<td><a href="{{ url_for('analyses.detail', analysis_id=a.id) }}" style="color:inherit;text-decoration:none;">{{ a.title }}</a></td>
|
|
<td style="color:#666;font-size:0.88rem;">{{ a.created_at.strftime('%d %b %Y') }}</td>
|
|
<td>{{ a.shot_count }}</td>
|
|
<td>{{ a.group_count }}</td>
|
|
<td>{{ "%.2f"|format(a.overall_stats.mean_speed) }} m/s</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p style="color:#888;margin-bottom:1.5rem;">No analyses yet.</p>
|
|
{% endif %}
|
|
|
|
{% if is_owner %}
|
|
<form method="post"
|
|
action="{{ url_for('sessions.upload_csv', session_id=session.id) }}"
|
|
enctype="multipart/form-data"
|
|
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
|
|
<div>
|
|
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Upload chronograph CSV</label>
|
|
<input type="file" name="csv_file" accept=".csv,text/csv" required style="font-size:0.9rem;">
|
|
</div>
|
|
<button type="submit"
|
|
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
|
Analyse & link
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% endblock %}
|