sprint3: dashboard counts per user + import owner_id + export visibility filter

This commit is contained in:
Agent Zero
2026-07-29 02:11:29 +02:00
parent 517e1b6d8b
commit b06aeeb720
3 changed files with 86 additions and 3 deletions
+63 -1
View File
@@ -2,11 +2,16 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
from app.core.visibility import apply_visibility_filter
from app.deps import require_permission from app.deps import require_permission
from app.models.contact import Contact
from app.plugins.registry import get_registry from app.plugins.registry import get_registry
router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"]) router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"])
@@ -36,3 +41,60 @@ async def list_dashboard_widgets(
widgets.sort(key=lambda w: w.get("order", 100)) widgets.sort(key=lambda w: w.get("order", 100))
return {"items": widgets, "total": len(widgets)} return {"items": widgets, "total": len(widgets)}
@router.get("/counts")
async def get_dashboard_counts(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_permission("dashboard:read")),
):
"""Get dashboard count statistics filtered by user visibility.
Returns counts for contacts and companies that the current user
is allowed to see based on ownership and sharing permissions.
"""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
# Contact count with visibility filter
contact_query = select(func.count(Contact.id)).where(
Contact.tenant_id == tenant_id,
Contact.deleted_at.is_(None),
)
contact_query = await apply_visibility_filter(
db, contact_query, "contact", Contact, user_id, tenant_id, is_system_admin
)
contact_result = await db.execute(contact_query)
contact_count = contact_result.scalar() or 0
# Company count (Contact.type == 'company') with visibility filter
company_query = select(func.count(Contact.id)).where(
Contact.tenant_id == tenant_id,
Contact.deleted_at.is_(None),
Contact.type == "company",
)
company_query = await apply_visibility_filter(
db, company_query, "contact", Contact, user_id, tenant_id, is_system_admin
)
company_result = await db.execute(company_query)
company_count = company_result.scalar() or 0
# Person count (Contact.type == 'person') with visibility filter
person_query = select(func.count(Contact.id)).where(
Contact.tenant_id == tenant_id,
Contact.deleted_at.is_(None),
Contact.type == "person",
)
person_query = await apply_visibility_filter(
db, person_query, "contact", Contact, user_id, tenant_id, is_system_admin
)
person_result = await db.execute(person_query)
person_count = person_result.scalar() or 0
return {
"contacts": contact_count,
"companies": company_count,
"persons": person_count,
"total": contact_count,
}
+8 -2
View File
@@ -92,15 +92,21 @@ async def export_data(
- format: 'csv' or 'xlsx' (default: csv) - format: 'csv' or 'xlsx' (default: csv)
""" """
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
# Determine filename based on entity_type # Determine filename based on entity_type
filename = f"{entity_type}_export.csv" filename = f"{entity_type}_export.csv"
# Fetch CSV data from the appropriate service function # Fetch CSV data from the appropriate service function
if entity_type == "contacts": if entity_type == "contacts":
csv_data = await import_export_service.export_contacts_csv(db, tenant_id) csv_data = await import_export_service.export_contacts_csv(
db, tenant_id, user_id=user_id, is_system_admin=is_system_admin
)
elif entity_type == "companies": elif entity_type == "companies":
csv_data = await import_export_service.export_companies_csv(db, tenant_id) csv_data = await import_export_service.export_companies_csv(
db, tenant_id, user_id=user_id, is_system_admin=is_system_admin
)
else: else:
raise HTTPException(status_code=400, detail=f"Unsupported entity_type: {entity_type}") raise HTTPException(status_code=400, detail=f"Unsupported entity_type: {entity_type}")
+15
View File
@@ -15,6 +15,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit from app.core.audit import log_audit
from app.core.visibility import apply_visibility_filter
from app.models.contact import Contact from app.models.contact import Contact
from app.services.contact_service import _serialize_contact as _contact_to_dict from app.services.contact_service import _serialize_contact as _contact_to_dict
@@ -86,6 +87,7 @@ async def import_companies(
phone_1=row.get("phone", "").strip() or None, phone_1=row.get("phone", "").strip() or None,
website=row.get("website", "").strip() or None, website=row.get("website", "").strip() or None,
description=row.get("description", "").strip() or None, description=row.get("description", "").strip() or None,
owner_id=user_id,
created_by=user_id, created_by=user_id,
updated_by=user_id, updated_by=user_id,
) )
@@ -162,6 +164,7 @@ async def import_contacts(
email_1=row.get("email", "").strip() or None, email_1=row.get("email", "").strip() or None,
phone_1=row.get("phone", "").strip() or None, phone_1=row.get("phone", "").strip() or None,
phone_2=row.get("mobile", "").strip() or None, phone_2=row.get("mobile", "").strip() or None,
owner_id=user_id,
created_by=user_id, created_by=user_id,
updated_by=user_id, updated_by=user_id,
) )
@@ -215,6 +218,8 @@ async def import_csv(
async def export_contacts_csv( async def export_contacts_csv(
db: AsyncSession, db: AsyncSession,
tenant_id: uuid.UUID, tenant_id: uuid.UUID,
user_id: uuid.UUID | None = None,
is_system_admin: bool = False,
) -> str: ) -> str:
"""Export contacts as CSV string using unified Contact model fields.""" """Export contacts as CSV string using unified Contact model fields."""
q = ( q = (
@@ -225,6 +230,10 @@ async def export_contacts_csv(
) )
.order_by(Contact.surname, Contact.firstname) .order_by(Contact.surname, Contact.firstname)
) )
if user_id:
q = await apply_visibility_filter(
db, q, "contact", Contact, user_id, tenant_id, is_system_admin
)
result = await db.execute(q) result = await db.execute(q)
contacts = result.scalars().all() contacts = result.scalars().all()
@@ -255,6 +264,8 @@ async def export_contacts_csv(
async def export_companies_csv( async def export_companies_csv(
db: AsyncSession, db: AsyncSession,
tenant_id: uuid.UUID, tenant_id: uuid.UUID,
user_id: uuid.UUID | None = None,
is_system_admin: bool = False,
) -> str: ) -> str:
"""Export companies (Contact.type == 'company') as CSV string.""" """Export companies (Contact.type == 'company') as CSV string."""
q = ( q = (
@@ -266,6 +277,10 @@ async def export_companies_csv(
) )
.order_by(Contact.name) .order_by(Contact.name)
) )
if user_id:
q = await apply_visibility_filter(
db, q, "contact", Contact, user_id, tenant_id, is_system_admin
)
result = await db.execute(q) result = await db.execute(q)
companies = result.scalars().all() companies = result.scalars().all()