feat: enhance project data handling with new Jinja filters and update UI labels for clarity

This commit is contained in:
2026-02-25 21:41:51 +00:00
parent 291fa8e862
commit bd3d937a82
4 changed files with 59 additions and 11 deletions

View File

@@ -2851,17 +2851,20 @@ async def upload_all_project_data(
Determine the grouping key for a file path. Determine the grouping key for a file path.
Files inside Auto_####/Auto_Leq/ or Auto_####/Auto_Lp_01/ are collapsed Files inside Auto_####/Auto_Leq/ or Auto_####/Auto_Lp_01/ are collapsed
up to their Auto_#### parent so they all land in the same session. up to their Auto_#### parent so they all land in the same session.
Only folder components are examined (not the filename, which is parts[-1]).
""" """
# Find the deepest Auto_#### component (case-insensitive) # Only look at folder components — exclude the filename (last part)
folder_parts = parts[:-1]
auto_idx = None auto_idx = None
for i, p in enumerate(parts): for i, p in enumerate(folder_parts):
if p.lower().startswith("auto_") and not p.lower().startswith("auto_leq") and not p.lower().startswith("auto_lp"): p_lower = p.lower()
if p_lower.startswith("auto_") and not p_lower.startswith("auto_leq") and not p_lower.startswith("auto_lp"):
auto_idx = i auto_idx = i
if auto_idx is not None: if auto_idx is not None:
# Group key = everything up to and including Auto_#### # Group key = everything up to and including Auto_####
return "/".join(parts[:auto_idx + 1]) return "/".join(folder_parts[:auto_idx + 1])
# Fallback: use the immediate parent folder # Fallback: use the immediate parent folder
return "/".join(parts[:-1]) if len(parts) > 1 else "" return "/".join(folder_parts) if folder_parts else ""
# --- Group files by session key --- # --- Group files by session key ---
groups: dict[str, list[tuple[str, bytes]]] = defaultdict(list) groups: dict[str, list[tuple[str, bytes]]] = defaultdict(list)

View File

@@ -5,6 +5,7 @@ All routers should import `templates` from this module to get consistent
filter and global function registration. filter and global function registration.
""" """
import json as _json
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
# Import timezone utilities # Import timezone utilities
@@ -32,8 +33,38 @@ def jinja_timezone_abbr():
# Create templates instance # Create templates instance
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
def jinja_local_date(dt, fmt="%m-%d-%y"):
"""Jinja filter: format a UTC datetime as a local date string (e.g. 02-19-26)."""
return format_local_datetime(dt, fmt)
def jinja_fromjson(s):
"""Jinja filter: parse a JSON string into a dict (returns {} on failure)."""
if not s:
return {}
try:
return _json.loads(s)
except Exception:
return {}
def jinja_same_date(dt1, dt2) -> bool:
"""Jinja global: True if two datetimes fall on the same local date."""
if not dt1 or not dt2:
return False
try:
d1 = format_local_datetime(dt1, "%Y-%m-%d")
d2 = format_local_datetime(dt2, "%Y-%m-%d")
return d1 == d2
except Exception:
return False
# Register Jinja filters and globals # Register Jinja filters and globals
templates.env.filters["local_datetime"] = jinja_local_datetime templates.env.filters["local_datetime"] = jinja_local_datetime
templates.env.filters["local_time"] = jinja_local_time templates.env.filters["local_time"] = jinja_local_time
templates.env.filters["local_date"] = jinja_local_date
templates.env.filters["fromjson"] = jinja_fromjson
templates.env.globals["timezone_abbr"] = jinja_timezone_abbr templates.env.globals["timezone_abbr"] = jinja_timezone_abbr
templates.env.globals["get_user_timezone"] = get_user_timezone templates.env.globals["get_user_timezone"] = get_user_timezone
templates.env.globals["same_date"] = jinja_same_date

View File

@@ -23,12 +23,26 @@
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path> <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path>
</svg> </svg>
<div> <div>
{% set meta = session.session_metadata|fromjson if session.session_metadata else {} %}
{% set is_manual = meta.get('source') in ('manual_upload', 'bulk_upload') %}
<div class="font-semibold text-gray-900 dark:text-white"> <div class="font-semibold text-gray-900 dark:text-white">
{{ session.started_at|local_datetime if session.started_at else 'Unknown Date' }} {% if location %}{{ location.name }}{% else %}Unknown Location{% endif %}
{% if session.started_at %}
&mdash;
{% if session.stopped_at and not same_date(session.started_at, session.stopped_at) %}
{{ session.started_at|local_date }} to {{ session.stopped_at|local_date }}
{% else %}
{{ session.started_at|local_date }}
{% endif %}
{% endif %}
</div> </div>
<div class="text-xs text-gray-500 dark:text-gray-400"> <div class="text-xs text-gray-500 dark:text-gray-400">
{% if unit %}{{ unit.id }}{% else %}Unknown Unit{% endif %} {% if is_manual %}
{% if location %} @ {{ location.name }}{% endif %} {% set store = meta.get('store_name') %}
Manual upload{% if store %} &mdash; Store {{ store }}{% endif %}
{% elif unit %}
{{ unit.id }}
{% endif %}
<span class="mx-2"></span> <span class="mx-2"></span>
{{ files|length }} file{{ 's' if files|length != 1 else '' }} {{ files|length }} file{{ 's' if files|length != 1 else '' }}
</div> </div>

View File

@@ -235,7 +235,7 @@
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg> </svg>
Upload All Upload Days
</button> </button>
<button onclick="htmx.trigger('#unified-files', 'refresh')" <button onclick="htmx.trigger('#unified-files', 'refresh')"
class="px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> class="px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
@@ -248,7 +248,7 @@
</div> </div>
</div> </div>
<!-- Upload All Panel --> <!-- Upload Days Panel -->
<div id="upload-all-panel" class="hidden border-b border-gray-200 dark:border-gray-700"> <div id="upload-all-panel" class="hidden border-b border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-800/50"> <div class="px-6 py-4 bg-gray-50 dark:bg-gray-800/50">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Bulk Import — Select Folder</p> <p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Bulk Import — Select Folder</p>
@@ -1575,7 +1575,7 @@ document.getElementById('schedule-modal')?.addEventListener('click', function(e)
} }
}); });
// ── Upload All ─────────────────────────────────────────────────────────────── // ── Upload Days ───────────────────────────────────────────────────────────────
function toggleUploadAll() { function toggleUploadAll() {
const panel = document.getElementById('upload-all-panel'); const panel = document.getElementById('upload-all-panel');