feat(projects): projects page overhaul — events, header, module toolbars, cards
Addresses a batch of projects-page UX issues: 1. Vibration Events sub-tab: add a Location filter + clickable column sorting (Timestamp/Location/Serial/Tran/Vert/Long/PVS/Mic). Events are cached client-side so location-filter and sort are instant (no SFM refetch). 3. Drop the misleading single-module "Sound Monitoring" subtitle on the Overview card (combined projects have multiple modules); show the project number · client identity instead. 4. Header cleanup: move the sound-only actions (Generate Combined Report, Night Report, Report Settings) and the Manual/Remote chip out of the global project header and into the Sound tab's module toolbar. The header now carries project-level concerns only (status, modules, merge). The Night Report / Report Settings modals stay defined in the header partial (global), so the relocated buttons still call them. 2. Per-module status UI: each module tab gets a status dropdown (active/on_hold/completed) wired to the new endpoint; the header module chips show a "✓ Done" / "On hold" badge. 5. Project cards redesigned: module mix accent strip, Sound/Vibration chips with per-module status, project number · client identity, and per-module "Sound"/"Vibration" quick-open buttons that deep-link into that module's tab (#sound / #vibration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ project.name }}</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{% if project_type %}
|
||||
{{ project_type.name }}
|
||||
{% else %}
|
||||
Project
|
||||
{% endif %}
|
||||
</p>
|
||||
{# Identity line — project number / client, not a module name. The
|
||||
enabled modules are already shown as chips in the page header. #}
|
||||
{% set _idbits = [] %}
|
||||
{% if project.project_number %}{% set _ = _idbits.append(project.project_number) %}{% endif %}
|
||||
{% if project.client_name %}{% set _ = _idbits.append(project.client_name) %}{% endif %}
|
||||
{% if _idbits %}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ _idbits | join(' · ') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if project.status == 'upcoming' %}
|
||||
<span class="px-3 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>
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
<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 Monitoring
|
||||
{% else %}{{ m }}{% endif %}
|
||||
{% set mstatus = (module_status or {}).get(m, 'active') %}
|
||||
{% if mstatus == 'completed' %}
|
||||
<span class="ml-0.5 inline-flex items-center px-1.5 py-0 rounded-full text-[10px] font-semibold bg-blue-200 text-blue-900 dark:bg-blue-800/70 dark:text-blue-100" title="This module is marked completed">✓ Done</span>
|
||||
{% elif mstatus == 'on_hold' %}
|
||||
<span class="ml-0.5 inline-flex items-center px-1.5 py-0 rounded-full text-[10px] font-semibold bg-amber-200 text-amber-900 dark:bg-amber-800/70 dark:text-amber-100" title="This module is on hold">On hold</span>
|
||||
{% endif %}
|
||||
<button onclick="removeModule('{{ m }}')" class="ml-0.5 hover:text-red-500 transition-colors" title="Remove module">
|
||||
<svg class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
@@ -47,50 +53,11 @@
|
||||
Add Module
|
||||
</button>
|
||||
</div>
|
||||
{% if project.data_collection_mode == 'remote' %}
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs 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="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>
|
||||
</svg>
|
||||
Remote
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-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="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
|
||||
</svg>
|
||||
Manual
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Project Actions -->
|
||||
<!-- Project Actions — project-level only. Sound-specific actions
|
||||
(Combined Report, Night Report, Report Settings) live in the Sound tab. -->
|
||||
<div class="flex items-center gap-3">
|
||||
{% if 'sound_monitoring' in modules %}
|
||||
<a href="/api/projects/{{ project.id }}/combined-report-wizard"
|
||||
class="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors flex items-center gap-2 text-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Generate Combined Report
|
||||
</a>
|
||||
<button onclick="openNightReportModal()"
|
||||
title="Last night's noise vs baseline, per location (FTP report pipeline)"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center gap-2 text-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
||||
</svg>
|
||||
Night Report
|
||||
</button>
|
||||
<button onclick="openReportSettings('{{ project.id }}')"
|
||||
title="Nightly report settings — schedule, baseline range, recipients"
|
||||
class="px-3 py-2 border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors flex items-center text-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="openMergeModal()"
|
||||
title="Merge this project into another (consolidates duplicates)"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors flex items-center gap-2 text-sm">
|
||||
|
||||
@@ -1,92 +1,108 @@
|
||||
<!-- Project List Grid -->
|
||||
{% if projects %}
|
||||
{% for item in projects %}
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<a href="/projects/{{ item.project.id }}" class="block p-6">
|
||||
<!-- Project Header -->
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{{ item.project.name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
||||
{% if item.project_type %}
|
||||
{% if item.project_type.id == 'sound_monitoring' %}
|
||||
<svg class="w-4 h-4 mr-1" 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.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
|
||||
</svg>
|
||||
{% elif item.project_type.id == 'vibration_monitoring' %}
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ item.project_type.name }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
<!-- Status Badge -->
|
||||
{% if item.project.status == 'active' %}
|
||||
<span class="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 item.project.status == 'on_hold' %}
|
||||
<span class="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 item.project.status == 'completed' %}
|
||||
<span class="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 item.project.status == 'archived' %}
|
||||
<span class="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>
|
||||
<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>
|
||||
|
||||
<!-- Project Description -->
|
||||
{% if item.project.description %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
{{ item.project.description }}
|
||||
</p>
|
||||
<!-- Module chips (with per-module status) -->
|
||||
{% if mods %}
|
||||
<div class="flex flex-wrap items-center gap-1.5 mb-3">
|
||||
{% for m in mods %}
|
||||
{% set st = mstatus.get(m, 'active') %}
|
||||
<span class="inline-flex items-center gap-1 pl-2 pr-2 py-0.5 rounded-full text-[11px] font-medium
|
||||
{% if m == 'sound_monitoring' %}bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300
|
||||
{% elif m == 'vibration_monitoring' %}bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300
|
||||
{% else %}bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300{% endif %}">
|
||||
{% if m == 'sound_monitoring' %}
|
||||
<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
|
||||
{% elif m == 'vibration_monitoring' %}
|
||||
<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
|
||||
{% else %}{{ m }}{% endif %}
|
||||
{% if st == 'completed' %}<span class="text-blue-600 dark:text-blue-300" title="Completed">✓</span>
|
||||
{% elif st == 'on_hold' %}<span class="opacity-70" title="On hold">⏸</span>{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Project Stats -->
|
||||
<div class="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<!-- Description -->
|
||||
{% if p.description %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">{{ p.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-3 gap-3 pt-4 border-t border-gray-100 dark:border-gray-700/60">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Locations</p>
|
||||
<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-xs text-gray-500 dark:text-gray-400">Units</p>
|
||||
<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>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Active</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{% if item.active_session_count > 0 %}
|
||||
<span class="text-green-600 dark:text-green-400">{{ item.active_session_count }}</span>
|
||||
{% else %}
|
||||
{{ item.active_session_count }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Active</p>
|
||||
<p class="text-lg font-semibold {% if item.active_session_count > 0 %}text-green-600 dark:text-green-400{% else %}text-gray-900 dark:text-white{% endif %}">{{ item.active_session_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Info -->
|
||||
{% if item.project.client_name %}
|
||||
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Client: <span class="font-medium text-gray-700 dark:text-gray-300">{{ item.project.client_name }}</span>
|
||||
</p>
|
||||
</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 %}
|
||||
|
||||
Reference in New Issue
Block a user