Files
terra-view/templates/partials/projects/project_list.html
T
serversdown 80464c6f11 feat(projects): per-module stat breakdown on project cards
The single Locations/Units/Active row was confusing: "Active" collided with
the green Active status badge and actually meant sound recording sessions, so
vibration-only projects showed a meaningless "Active 0", and combined projects
lumped both modules together with no split.

Cards now show one stat line per module, each carrying its own identity +
status badge (so the separate chip row is dropped as redundant):
  Vibration   N locations · M units
  Sound       N NRLs · M units · K recording

- /list endpoint computes module_stats: locations (active, by type) and units
  counted via a join on the assigned location's type — so a module's unit
  count always reconciles with its location count (verified: sound+vibration
  units == total active assignments for every project).
- "recording" (active sessions) shows only under Sound, where it's meaningful.
- Projects with no modules fall back to a simple Locations/Units row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
2026-06-22 20:38:48 +00:00

129 lines
9.0 KiB
HTML

<!-- Project List Grid -->
{% if projects %}
{% for item in projects %}
{% set p = item.project %}
{% set mods = item.modules or [] %}
{% set mstatus = item.module_status or {} %}
{% set has_sound = 'sound_monitoring' in mods %}
{% set has_vib = 'vibration_monitoring' in mods %}
<div class="group relative flex flex-col bg-white dark:bg-slate-800 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 overflow-hidden">
<!-- Accent strip — reflects the project's module mix -->
<div class="absolute inset-x-0 top-0 h-1
{% if has_sound and has_vib %}bg-gradient-to-r from-seismo-orange to-blue-500
{% elif has_sound %}bg-seismo-orange
{% elif has_vib %}bg-blue-500
{% else %}bg-gray-300 dark:bg-gray-600{% endif %}"></div>
<a href="/projects/{{ p.id }}" class="block flex-1 p-6 pt-7">
<!-- Header: name + identity + status -->
<div class="flex items-start justify-between gap-3 mb-3">
<div class="min-w-0">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">{{ p.name }}</h3>
{% set idbits = [] %}
{% if p.project_number %}{% set _ = idbits.append(p.project_number) %}{% endif %}
{% if p.client_name %}{% set _ = idbits.append(p.client_name) %}{% endif %}
{% if idbits %}
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">{{ idbits | join(' · ') }}</p>
{% endif %}
</div>
{% if p.status == 'active' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 rounded-full">Active</span>
{% elif p.status == 'upcoming' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 rounded-full">Upcoming</span>
{% elif p.status == 'on_hold' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">On Hold</span>
{% elif p.status == 'completed' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 rounded-full">Completed</span>
{% elif p.status == 'archived' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400 rounded-full">Archived</span>
{% endif %}
</div>
<!-- Description -->
{% if p.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">{{ p.description }}</p>
{% endif %}
<!-- Per-module stats — each module shows only its own, relevant counts.
These lines double as the module legend (identity + status), so a
separate chip row would be redundant. -->
{% set ms = item.module_stats %}
{% if ms %}
<div class="space-y-2.5 pt-4 border-t border-gray-100 dark:border-gray-700/60">
{% if 'vibration' in ms %}
{% set vst = mstatus.get('vibration_monitoring', 'active') %}
<div class="flex items-baseline gap-2.5 text-sm flex-wrap">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Vibration
{% if vst == 'completed' %}<span title="Completed"></span>{% elif vst == 'on_hold' %}<span class="opacity-80" title="On hold"></span>{% endif %}
</span>
<span class="text-gray-500 dark:text-gray-400">
<b class="font-semibold text-gray-900 dark:text-white tabular-nums">{{ ms.vibration.locations }}</b> location{{ '' if ms.vibration.locations == 1 else 's' }}
· <b class="font-semibold text-gray-900 dark:text-white tabular-nums">{{ ms.vibration.units }}</b> unit{{ '' if ms.vibration.units == 1 else 's' }}
</span>
</div>
{% endif %}
{% if 'sound' in ms %}
{% set sst = mstatus.get('sound_monitoring', 'active') %}
<div class="flex items-baseline gap-2.5 text-sm flex-wrap">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M12 6v12M9 8.464a5 5 0 000 7.072"/></svg>
Sound
{% if sst == 'completed' %}<span title="Completed"></span>{% elif sst == 'on_hold' %}<span class="opacity-80" title="On hold"></span>{% endif %}
</span>
<span class="text-gray-500 dark:text-gray-400">
<b class="font-semibold text-gray-900 dark:text-white tabular-nums">{{ ms.sound.locations }}</b> NRL{{ '' if ms.sound.locations == 1 else 's' }}
· <b class="font-semibold text-gray-900 dark:text-white tabular-nums">{{ ms.sound.units }}</b> unit{{ '' if ms.sound.units == 1 else 's' }}
· {% if ms.sound.recording > 0 %}<b class="font-semibold text-green-600 dark:text-green-400 tabular-nums">{{ ms.sound.recording }}</b> recording{% else %}<span class="text-gray-400 dark:text-gray-500">0 recording</span>{% endif %}
</span>
</div>
{% endif %}
</div>
{% else %}
<!-- Fallback: project with no modules enabled yet -->
<div class="grid grid-cols-2 gap-3 pt-4 border-t border-gray-100 dark:border-gray-700/60">
<div>
<p class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Locations</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ item.location_count }}</p>
</div>
<div>
<p class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Units</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ item.unit_count }}</p>
</div>
</div>
{% endif %}
</a>
<!-- Per-module quick-open footer — jumps straight into that module's tab -->
{% if has_sound or has_vib %}
<div class="flex items-stretch border-t border-gray-100 dark:border-gray-700/60 divide-x divide-gray-100 dark:divide-gray-700/60">
{% if has_sound %}
<a href="/projects/{{ p.id }}#sound"
class="flex-1 flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-orange-600 dark:text-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-colors">
<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="M15.536 8.464a5 5 0 010 7.072M12 6v12M9 8.464a5 5 0 000 7.072"/></svg>
Sound
</a>
{% endif %}
{% if has_vib %}
<a href="/projects/{{ p.id }}#vibration"
class="flex-1 flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors">
<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="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Vibration
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<!-- Empty State -->
<div class="col-span-full flex flex-col items-center justify-center py-12 text-gray-400 dark:text-gray-500">
<svg class="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
</svg>
<p class="text-lg font-medium">No projects found</p>
<p class="text-sm mt-1">Create your first project to get started</p>
</div>
{% endif %}