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
This commit is contained in:
@@ -470,8 +470,10 @@ async def get_projects_list(
|
||||
for project in projects:
|
||||
# Get project type
|
||||
project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first()
|
||||
mods = _get_project_modules(project.id, db)
|
||||
|
||||
# Count locations
|
||||
# Count locations (project-wide, includes removed — kept for back-compat
|
||||
# with the compact list view).
|
||||
location_count = db.query(func.count(MonitoringLocation.id)).filter_by(
|
||||
project_id=project.id
|
||||
).scalar()
|
||||
@@ -484,7 +486,7 @@ async def get_projects_list(
|
||||
)
|
||||
).scalar()
|
||||
|
||||
# Count active sessions
|
||||
# Count active (recording) sessions — a sound-monitoring concept
|
||||
active_session_count = db.query(func.count(MonitoringSession.id)).filter(
|
||||
and_(
|
||||
MonitoringSession.project_id == project.id,
|
||||
@@ -492,11 +494,46 @@ async def get_projects_list(
|
||||
)
|
||||
).scalar()
|
||||
|
||||
# Per-module stats — each module shows only its own, relevant counts.
|
||||
# Locations: active only (removed_at IS NULL). Units: active assignments
|
||||
# counted by the TYPE OF LOCATION they sit on (join), so a module's unit
|
||||
# count always lines up with its own location count — independent of the
|
||||
# assignment's denormalized device_type.
|
||||
def _module_loc_count(loc_type):
|
||||
return db.query(func.count(MonitoringLocation.id)).filter(
|
||||
MonitoringLocation.project_id == project.id,
|
||||
MonitoringLocation.location_type == loc_type,
|
||||
MonitoringLocation.removed_at.is_(None),
|
||||
).scalar()
|
||||
|
||||
def _module_unit_count(loc_type):
|
||||
return db.query(func.count(UnitAssignment.id)).join(
|
||||
MonitoringLocation, MonitoringLocation.id == UnitAssignment.location_id
|
||||
).filter(
|
||||
UnitAssignment.project_id == project.id,
|
||||
UnitAssignment.assigned_until.is_(None),
|
||||
MonitoringLocation.location_type == loc_type,
|
||||
).scalar()
|
||||
|
||||
module_stats = {}
|
||||
if "vibration_monitoring" in mods:
|
||||
module_stats["vibration"] = {
|
||||
"locations": _module_loc_count("vibration"),
|
||||
"units": _module_unit_count("vibration"),
|
||||
}
|
||||
if "sound_monitoring" in mods:
|
||||
module_stats["sound"] = {
|
||||
"locations": _module_loc_count("sound"),
|
||||
"units": _module_unit_count("sound"),
|
||||
"recording": active_session_count,
|
||||
}
|
||||
|
||||
projects_data.append({
|
||||
"project": project,
|
||||
"project_type": project_type,
|
||||
"modules": _get_project_modules(project.id, db),
|
||||
"modules": mods,
|
||||
"module_status": _get_module_statuses(project.id, db),
|
||||
"module_stats": module_stats,
|
||||
"location_count": location_count,
|
||||
"unit_count": unit_count,
|
||||
"active_session_count": active_session_count,
|
||||
|
||||
Reference in New Issue
Block a user